From: David Polakovic Date: Tue, 24 Feb 2026 14:47:16 +0000 (+0100) Subject: feat: Initial commit since server migration. X-Git-Tag: 1.0.0 X-Git-Url: https://git.dpolakovic.space/%22%22.esc_url%28%24base_url%29.%22//%22%22/%22%22.esc_url%28%24base_url%29.%22//%22%22?a=commitdiff_plain;h=7be159980aeda58ae94a98785ed9134538a07448;p=dpolakovic-space feat: Initial commit since server migration. --- 7be159980aeda58ae94a98785ed9134538a07448 diff --git a/Git-server/Coffee/coffee-counter.js b/Git-server/Coffee/coffee-counter.js new file mode 100644 index 0000000..7bf3b7f --- /dev/null +++ b/Git-server/Coffee/coffee-counter.js @@ -0,0 +1,119 @@ +/** + * + * @licstart The following is the entire license notice for the + * JavaScript code in this page. + * + * Copyright (C) 2025 David Polakovic + * + * + * The JavaScript code in this page is free software: you can + * redistribute it and/or modify it under the terms of the GNU + * General Public License (GNU GPL) as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) + * any later version. The code is distributed WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. + * + * As additional permission under GNU GPL version 3 section 7, you + * may distribute non-source (e.g., minimized or compacted) forms of + * that code without the copy of the GNU GPL normally required by + * section 4, provided you include this license notice and a URL + * through which recipients can access the Corresponding Source. + * + * @licend The above is the entire license notice + * for the JavaScript code in this page. + * + */ + +// coffee-counter.js - Reusable coffee counter script with timezone-aware counting + +(function() { + // Configuration + const API_URL = 'https://git.dpolakovic.space'; + const REFRESH_INTERVAL = 5000; // 5 seconds + + // Get user's local midnight timestamp + function getLocalMidnight() { + const now = new Date(); + const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0); + return Math.floor(midnight.getTime() / 1000); // Convert to Unix timestamp + } + + // Function to update all counter elements + async function updateCounters() { + try { + const response = await fetch(`${API_URL}/pot0`, { + method: 'STATS', + headers: { + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Failed to fetch coffee stats'); + } + + const data = await response.json(); + const allBrews = data.brews || []; // Array of Unix timestamps + + // Filter brews since user's local midnight + const userMidnight = getLocalMidnight(); + const todayBrews = allBrews.filter(timestamp => timestamp >= userMidnight); + const count = todayBrews.length; + + console.log('User midnight:', new Date(userMidnight * 1000)); + console.log('Total brews in 48h:', allBrews.length); + console.log('Brews since user midnight:', count); + + // Update simple counters (just the number) + const simpleElements = document.querySelectorAll('.coffee-counter'); + simpleElements.forEach(element => { + element.textContent = count; + element.classList.remove('loading'); + }); + + // Update verbose counters (full sentence with HTML support) + const verboseElements = document.querySelectorAll('.coffee-counter-verbose'); + verboseElements.forEach(element => { + let html; + if (count === 0) { + html = ', but served 0 coffees today. Do you want a coffee?'; + } else if (count === 1) { + html = ' but served just 1 coffee today. Do you want a coffee too?'; + } else { + html = ` and served ${count} coffees today. Do you want a coffee?`; + } + element.innerHTML = html; + element.classList.remove('loading'); + }); + + } catch (error) { + console.error('Coffee counter error:', error); + + // Mark simple counters as error + const simpleElements = document.querySelectorAll('.coffee-counter'); + simpleElements.forEach(element => { + element.textContent = '?'; + element.classList.add('error'); + }); + + // Mark verbose counters as error + const verboseElements = document.querySelectorAll('.coffee-counter-verbose'); + verboseElements.forEach(element => { + element.innerHTML = 'error loading counter'; + element.classList.add('error'); + }); + } + } + + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + updateCounters(); + setInterval(updateCounters, REFRESH_INTERVAL); + }); + } else { + updateCounters(); + setInterval(updateCounters, REFRESH_INTERVAL); + } +})(); \ No newline at end of file diff --git a/Git-server/Coffee/coffee-server.php b/Git-server/Coffee/coffee-server.php new file mode 100644 index 0000000..6142ea9 --- /dev/null +++ b/Git-server/Coffee/coffee-server.php @@ -0,0 +1,529 @@ + 'cream', + 'half-and-half' => 'half-and-half', + 'whole-milk' => 'whole milk', + 'part-skim' => 'part skim', + 'skim-milk' => 'skim milk', + 'non-dairy' => 'non-dairy', + 'oat-milk' => 'oat milk', + 'pea-milk' => 'pea milk', + 'rye-milk' => 'rye milk', + 'almond-milk' => 'almond milk', + 'soy-milk' => 'soy milk', + 'coconut-milk' => 'coconut milk', + 'pumpkin-puree' => 'pumpkin puree', + 'marple-syrup' => 'marple syrup', + 'vanilla' => 'vanilla', + 'cinnamon' => 'cinnamon', + 'raspberry' => 'raspberry', + 'chocolate' => 'chocolate', + 'whisky' => 'whisky', + 'rum' => 'rum', + 'kahlua' => 'Kahlua', + 'aquavit' => 'Aquavit' +]; + +// Max total additions capacity in ml +$maxAdditionsCapacity = 200; + +$counterFile = '/var/www/html/dpolakovic.git/coffee/coffee_counter.json'; +$potStateFile = '/var/www/html/dpolakovic.git/coffee/pot_state.json'; + +// Canonical pot identifiers used as keys in state files +$potNames = ['pot-0', 'pot-1']; + +$potDisplayNames = [ + 'pot-0' => 'red coffee pot', + 'pot-1' => 'white coffee pot' +]; + +$potDetails = [ + 'pot-0' => 'brand new coffee pot in scarlett red color', + 'pot-1' => 'older, but still usable white coffee pot, used when red one is not available' +]; + +$potCapacities = [ + 'pot-0' => 6, + 'pot-1' => 8 +]; + +$totalPots = count($potNames); + +// Map every accepted URL slug to its canonical pot name +// Parentheses in pot-(-2147483648) are an integer underflow joke +$potAliases = [ + 'pot-0' => 'pot-0', + 'pot0' => 'pot-0', + 'pot-2' => 'pot-0', + 'pot2' => 'pot-0', + 'pot--2147483648 ' => 'pot-0', + 'pot-n2147483648 ' => 'pot-0', + 'red-coffee-pot' => 'pot-0', + 'pot-1' => 'pot-1', + 'pot1' => 'pot-1', + 'white-coffee-pot' => 'pot-1' +]; + +$allPotSlugs = array_keys($potAliases); + +// ========================================================== +// POT STATE FUNCTIONS +// ========================================================== + +// State machine: ready -> being cleaned -> empty -> ready +// Transitions are time-based or triggered by POST (refill) + +// Load pot state from JSON file, create default if missing +function loadPotState() { + global $potStateFile, $potNames, $potCapacities; + + if (!file_exists($potStateFile)) { + $state = []; + foreach ($potNames as $name) { + $state[$name] = [ + 'status' => 'ready', + 'coffees_left' => $potCapacities[$name], + 'last_used' => 0, + 'state_change_time' => 0 + ]; + } + file_put_contents($potStateFile, json_encode($state)); + return $state; + } + + return json_decode(file_get_contents($potStateFile), true); +} + +// Persist pot state to JSON file +function savePotState($state) { + global $potStateFile; + file_put_contents($potStateFile, json_encode($state)); +} + +// Advance pot states based on elapsed time +// Uses elseif to prevent double-transition in one request +// Skips auto-refill for BREW target so empty pots refuse BREW +function updatePotStatuses($state, $currentMethod = '', $targetPot = null) { + global $potNames, $potCapacities; + + foreach ($potNames as $name) { + $elapsed = time() - $state[$name]['state_change_time']; + + if ($state[$name]['status'] === 'being cleaned' && $elapsed >= 10) { + $state[$name]['status'] = 'empty'; + $state[$name]['state_change_time'] = time(); + } elseif ($state[$name]['status'] === 'empty' && $elapsed >= 60) { + if ($currentMethod === 'BREW' && ($targetPot === $name || $targetPot === null)) { + continue; + } + $state[$name]['status'] = 'ready'; + $state[$name]['coffees_left'] = $potCapacities[$name]; + $state[$name]['state_change_time'] = time(); + } + } + + return $state; +} + +// ========================================================== +// COUNTER FUNCTIONS +// ========================================================== + +// Load brew timestamps and prune entries older than 48 hours +function getBrewHistory() { + global $counterFile; + + if (!file_exists($counterFile)) { + $data = ['brews' => []]; + file_put_contents($counterFile, json_encode($data)); + return $data; + } + + $data = json_decode(file_get_contents($counterFile), true); + + $cutoffTime = time() - (48 * 60 * 60); + $data['brews'] = array_filter($data['brews'], function($timestamp) use ($cutoffTime) { + return $timestamp > $cutoffTime; + }); + + $data['brews'] = array_values($data['brews']); + file_put_contents($counterFile, json_encode($data)); + + return $data; +} + +// Append current timestamp to brew history +function addBrew() { + global $counterFile; + + $data = getBrewHistory(); + $data['brews'][] = time(); + + file_put_contents($counterFile, json_encode($data)); + + return count($data['brews']); +} + +// ========================================================== +// ADDITIONS PARSER +// ========================================================== + +// Parse Accept-Additions header into array of name/q pairs +// Splits on comma, extracts optional ;q= quality value +function parseAdditions($acceptAdditions) { + if (empty($acceptAdditions)) { + return []; + } + + $additions = []; + $parts = explode(',', $acceptAdditions); + + foreach ($parts as $part) { + $part = trim($part); + preg_match('/([^;]+)(;q=([0-9.]+))?/', $part, $matches); + if (!empty($matches[1])) { + $addition = trim($matches[1]); + $quality = isset($matches[3]) ? floatval($matches[3]) : 1.0; + $additions[] = ['name' => $addition, 'q' => $quality]; + } + } + + usort($additions, function($a, $b) { + return $b['q'] <=> $a['q']; + }); + + return $additions; +} + +// ========================================================== +// RESOLVE POT FROM URI +// ========================================================== + +// Match URI against all known pot slugs including parenthesized +// Uses preg_quote so special chars like () are escaped properly +$slugPattern = implode('|', array_map('preg_quote', $allPotSlugs)); +preg_match('/\/(' . $slugPattern . ')/', $requestUri, $matches); + +$potName = null; +if (isset($matches[1]) && isset($potAliases[$matches[1]])) { + $potName = $potAliases[$matches[1]]; +} + +// ========================================================== +// LOAD AND UPDATE STATE +// ========================================================== + +$potState = loadPotState(); +$potState = updatePotStatuses($potState, $method, $potName); +savePotState($potState); + +// ========================================================== +// WHEN METHOD +// ========================================================== + +// Return 501 for WHEN method per RFC 2324 section 3 +if ($method === 'WHEN') { + http_response_code(501); + header('Content-Type: text/plain'); + echo "Error 501 WHEN Not Implemented. Use BREW with declared addition quantity according to RFC2324.\n"; + exit; +} + +// ========================================================== +// PROPFIND ON ROOT (discover all pots) +// ========================================================== + +// Return server capabilities and pot listing when no pot specified +// STATS method intentionally omitted from listed methods +if ($method === 'PROPFIND' && $potName === null) { + http_response_code(200); + header('Content-Type: application/json'); + + $capabilities = [ + 'protocol' => 'HTCPCP 1.0', + 'rfc' => '2324', + 'methods' => ['BREW', 'GET', 'POST', 'PROPFIND'], + 'additions_supported' => array_values($additionDisplayNames), + 'max_additions_capacity_ml' => $maxAdditionsCapacity, + 'total_pots' => $totalPots, + 'pots' => [ + [ + 'canonical' => 'pot-0', + 'aliases' => ['pot-0', 'pot0', 'pot-2', 'pot2', 'pot-(-2147483648)', 'red-coffee-pot'], + 'display_name' => $potDisplayNames['pot-0'], + 'capacity' => $potCapacities['pot-0'] + ], + [ + 'canonical' => 'pot-1', + 'aliases' => ['pot-1', 'pot1', 'white-coffee-pot'], + 'display_name' => $potDisplayNames['pot-1'], + 'capacity' => $potCapacities['pot-1'] + ] + ], + 'auto_assignment' => 'Send BREW request without pot name for automatic assignment' + ]; + + echo json_encode($capabilities, JSON_PRETTY_PRINT) . "\n"; + exit; +} + +// ========================================================== +// AUTO POT ASSIGNMENT +// ========================================================== + +// Assign first available ready pot when BREW sent to root +if ($potName === null && $method === 'BREW') { + $freePot = null; + + foreach ($potNames as $name) { + if ($potState[$name]['status'] === 'ready') { + $freePot = $name; + break; + } + } + + if ($freePot !== null) { + $potName = $freePot; + } else { + http_response_code(503); + header('Content-Type: text/plain'); + echo "Error 503 No Coffee Pot Available. Try GET request for pot-0 or pot-1 to check their status.\n"; + exit; + } +} + +// ========================================================== +// VALIDATE POT NAME +// ========================================================== + +// Reject requests to unknown pot identifiers +if ($potName === null || !in_array($potName, $potNames)) { + http_response_code(404); + header('Content-Type: text/plain'); + $allAliases = implode(', ', $allPotSlugs); + echo "Error 404 Coffee Pot Not Found. Available pots: $allAliases\n"; + exit; +} + +$potStatus = $potState[$potName]['status']; +$potDisplay = $potDisplayNames[$potName]; + +// ========================================================== +// METHOD SWITCH +// ========================================================== + +switch ($method) { + case 'BREW': + // Refuse if pot is not ready (being cleaned or empty) + if ($potStatus !== 'ready') { + http_response_code(503); + header('Content-Type: text/plain'); + echo "Error 503 Service Unavailable. The $potDisplay ($potName) is currently $potStatus.\n"; + exit; + } + + // Validate additions against supported list (case-insensitive) + $requestedAdditions = parseAdditions($acceptAdditions); + $approvedAdditions = []; + $unsupportedAdditions = []; + + foreach ($requestedAdditions as $addition) { + $normalizedName = strtolower($addition['name']); + if (in_array($normalizedName, $availableAdditions)) { + $approvedAdditions[] = [ + 'name' => $additionDisplayNames[$normalizedName], + 'q' => $addition['q'] + ]; + } else { + $unsupportedAdditions[] = $addition['name']; + } + } + + if (!empty($unsupportedAdditions)) { + http_response_code(406); + header('Content-Type: text/plain'); + echo "Error 406 Not Acceptable. We do not offer any " . implode(', ', $unsupportedAdditions) . ".\n"; + exit; + } + + // Reject if total addition volume exceeds cup capacity + // Sum of q values treated as ml + if (!empty($approvedAdditions)) { + $totalQ = 0; + foreach ($approvedAdditions as $add) { + $totalQ += $add['q']; + } + if ($totalQ > $maxAdditionsCapacity) { + http_response_code(507); + header('Content-Type: text/plain'); + echo "Error 507 Insufficient (Cup) Storage. Our cups can handle up to {$maxAdditionsCapacity}ml of additions.\n"; + exit; + } + } + + // Decrement remaining coffees and trigger cleaning if empty + $potState[$potName]['coffees_left']--; + $potState[$potName]['last_used'] = time(); + + if ($potState[$potName]['coffees_left'] <= 0) { + $potState[$potName]['status'] = 'being cleaned'; + $potState[$potName]['state_change_time'] = time(); + } + + savePotState($potState); + addBrew(); + + // Build and send brew response + sleep(1); + http_response_code(200); + header('Content-Type: message/coffeepot'); + header('X-Coffee-Pot: ' . $potName); + header('X-Coffee-Pot-Name: ' . $potDisplay); + + $additionNames = array_map(function($a) { return $a['name']; }, $approvedAdditions); + + if (!empty($additionNames)) { + header('Additions: ' . implode(', ', $additionNames)); + } + + // Format additions list with "and" before last item + echo "Your coffee"; + if (!empty($additionNames)) { + if (count($additionNames) === 1) { + $additionsStr = $additionNames[0]; + } else { + $last = array_pop($additionNames); + $additionsStr = implode(', ', $additionNames) . ' and ' . $last; + } + echo " with " . $additionsStr; + } + echo " is ready from the $potDisplay ($potName). Enjoy!\n"; + if ($potState[$potName]['status'] === 'being cleaned') { + echo "Note: $potDisplay is now being cleaned and will be available shortly.\n"; + } + break; + + // Return pot status as JSON + case 'GET': + http_response_code(200); + header('Content-Type: application/json'); + + $status = [ + 'pot_id' => $potName, + 'pot_name' => $potDisplay, + 'status' => $potStatus, + 'coffees_left' => $potState[$potName]['coffees_left'], + 'capacity' => $potCapacities[$potName] . ' cups', + 'details' => $potDetails[$potName], + 'aliases' => array_keys(array_filter($potAliases, function($v) use ($potName) { return $v === $potName; })) + ]; + + echo json_encode($status, JSON_PRETTY_PRINT) . "\n"; + break; + + // Refill coffee pot (add beans) + // Only works when pot is empty, not during cleaning + case 'POST': + if ($potStatus === 'ready') { + http_response_code(200); + header('Content-Type: text/plain'); + echo "$potDisplay ($potName) is already full and ready. No refill needed.\n"; + exit; + } + + if ($potStatus === 'being cleaned') { + http_response_code(503); + header('Content-Type: text/plain'); + echo "Error 503 Service Unavailable. $potDisplay ($potName) is currently being cleaned. Please wait.\n"; + exit; + } + + $potState[$potName]['status'] = 'ready'; + $potState[$potName]['coffees_left'] = $potCapacities[$potName]; + $potState[$potName]['state_change_time'] = time(); + savePotState($potState); + + http_response_code(200); + header('Content-Type: text/plain'); + echo "Coffee beans added to $potDisplay ($potName). Pot is now ready.\n"; + break; + + // Return pot capabilities as JSON + case 'PROPFIND': + http_response_code(200); + header('Content-Type: application/json'); + + $capabilities = [ + 'pot_id' => $potName, + 'pot_name' => $potDisplay, + 'protocol' => 'HTCPCP 1.0', + 'rfc' => '2324', + 'methods' => ['BREW', 'GET', 'POST', 'PROPFIND'], + 'additions_supported' => array_values($additionDisplayNames), + 'max_additions_capacity_ml' => $maxAdditionsCapacity, + 'total_pots' => $totalPots, + 'pot_names' => $potNames, + 'auto_assignment' => 'Send BREW request without pot name for automatic assignment' + ]; + + echo json_encode($capabilities, JSON_PRETTY_PRINT) . "\n"; + break; + + // Return brew timestamps for counter widget + // CORS header needed for cross-origin JS fetch + // Not listed in PROPFIND methods to keep it internal + case 'STATS': + http_response_code(200); + header('Content-Type: application/json'); + header('Access-Control-Allow-Origin: *'); + + $brewData = getBrewHistory(); + + echo json_encode([ + 'brews' => $brewData['brews'], + 'total_48h' => count($brewData['brews']) + ], JSON_PRETTY_PRINT); + break; + + default: + http_response_code(501); + header('Content-Type: text/plain'); + echo "Error 501 Method $method Not Implemented\n"; +} +?> \ No newline at end of file diff --git a/Git-server/Gitweb/banner.html b/Git-server/Gitweb/banner.html new file mode 100644 index 0000000..f5399e4 --- /dev/null +++ b/Git-server/Gitweb/banner.html @@ -0,0 +1,101 @@ + + + diff --git a/Git-server/Gitweb/footer.html b/Git-server/Gitweb/footer.html new file mode 100644 index 0000000..f52e5fd --- /dev/null +++ b/Git-server/Gitweb/footer.html @@ -0,0 +1,18 @@ + +
+ Copyright 2022-2026 David Polakovic. Individual project licenses are located in project + root in full length. +

+ Site generated using Gitweb. Read + the documentation + for JavaScript and cookie information. + Additional source code available + here + under + GPLv3 license. +

+ Server for this subdomain is + RFC 2324 + compliant + +
diff --git a/Git-server/Gitweb/gitweb.cgi b/Git-server/Gitweb/gitweb.cgi new file mode 100644 index 0000000..7987979 --- /dev/null +++ b/Git-server/Gitweb/gitweb.cgi @@ -0,0 +1,8558 @@ +#!/usr/bin/perl + +# gitweb - simple web interface to track changes in git repositories +# +# (C) 2005-2006, Kay Sievers +# (C) 2005, Christian Gierke +# +# This program is licensed under the GPLv2 + +use 5.008001; +use strict; +use warnings; +# handle ACL in file access tests +use filetest 'access'; +use CGI qw(:standard :escapeHTML -nosticky); +use CGI::Util qw(unescape); +use CGI::Carp qw(fatalsToBrowser set_message); +use Encode; +use Fcntl ':mode'; +use File::Find qw(); +use File::Basename qw(basename); +use Time::HiRes qw(gettimeofday tv_interval); +use Digest::MD5 qw(md5_hex); + +binmode STDOUT, ':utf8'; + +if (!defined($CGI::VERSION) || $CGI::VERSION < 4.08) { + eval 'sub CGI::multi_param { CGI::param(@_) }' +} + +our $t0 = [ gettimeofday() ]; +our $number_of_git_cmds = 0; + +BEGIN { + CGI->compile() if $ENV{'MOD_PERL'}; +} + +our $version = "2.47.3"; + +our ($my_url, $my_uri, $base_url, $path_info, $home_link); +sub evaluate_uri { + our $cgi; + + our $my_url = $cgi->url(); + our $my_uri = $cgi->url(-absolute => 1); + + # Base URL for relative URLs in gitweb ($logo, $favicon, ...), + # needed and used only for URLs with nonempty PATH_INFO + our $base_url = $my_url; + + # When the script is used as DirectoryIndex, the URL does not contain the name + # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we + # have to do it ourselves. We make $path_info global because it's also used + # later on. + # + # Another issue with the script being the DirectoryIndex is that the resulting + # $my_url data is not the full script URL: this is good, because we want + # generated links to keep implying the script name if it wasn't explicitly + # indicated in the URL we're handling, but it means that $my_url cannot be used + # as base URL. + # Therefore, if we needed to strip PATH_INFO, then we know that we have + # to build the base URL ourselves: + our $path_info = decode_utf8($ENV{"PATH_INFO"}); + if ($path_info) { + # $path_info has already been URL-decoded by the web server, but + # $my_url and $my_uri have not. URL-decode them so we can properly + # strip $path_info. + $my_url = unescape($my_url); + $my_uri = unescape($my_uri); + if ($my_url =~ s,\Q$path_info\E$,, && + $my_uri =~ s,\Q$path_info\E$,, && + defined $ENV{'SCRIPT_NAME'}) { + $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'}; + } + } + + # target of the home link on top of all pages + our $home_link = $my_uri || "/"; +} + +# core git executable to use +# this can just be "git" if your webserver has a sensible PATH +our $GIT = "/usr/bin/git"; + +# absolute fs-path which will be prepended to the project path +#our $projectroot = "/pub/scm"; +our $projectroot = "/pub/git"; + +# fs traversing limit for getting project list +# the number is relative to the projectroot +our $project_maxdepth = 2007; + +# string of the home link on top of all pages +our $home_link_str = $ENV{"SERVER_NAME"} ? "git://" . $ENV{"SERVER_NAME"} : "projects"; + +# extra breadcrumbs preceding the home link +our @extra_breadcrumbs = (); + +# name of your site or organization to appear in page titles +# replace this with something more descriptive for clearer bookmarks +our $site_name = "" + || ($ENV{'SERVER_NAME'} || "Untitled") . " Git"; + +# html snippet to include in the section of each page +our $site_html_head_string = ""; +# filename of html text to include at top of each page +our $site_header = "banner.html"; +# html text to include at home page +our $home_text = "indextext.html"; +# filename of html text to include at bottom of each page +our $site_footer = "footer.html"; + +# URI of stylesheets +our @stylesheets = ("static/gitweb.css"); +# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG. +our $stylesheet = undef; +# URI of GIT logo (72x27 size) +our $logo = "static/git-logo.png"; +# URI of GIT favicon, assumed to be image/png type +our $favicon = "static/git-favicon.png"; +# URI of gitweb.js (JavaScript code for gitweb) +our $javascript = "static/gitweb.js"; + +# URI and label (title) of GIT logo link +#our $logo_url = "https://www.kernel.org/pub/software/scm/git/docs/"; +#our $logo_label = "git documentation"; +our $logo_url = "https://git-scm.com/"; +our $logo_label = "git homepage"; + +# source of projects list +our $projects_list = ""; + +# the width (in characters) of the projects list "Description" column +our $projects_list_description_width = 50; + +# group projects by category on the projects list +# (enabled if this variable evaluates to true) +our $projects_list_group_categories = 0; + +# default category if none specified +# (leave the empty string for no category) +our $project_list_default_category = ""; + +# default order of projects list +# valid values are none, project, descr, owner, and age +our $default_projects_order = "descr"; + +# show repository only if this file exists +# (only effective if this variable evaluates to true) +our $export_ok = ""; + +# don't generate age column on the projects list page +our $omit_age_column = 0; + +# don't generate information about owners of repositories +our $omit_owner=1; + +# show repository only if this subroutine returns true +# when given the path to the project, for example: +# sub { return -e "$_[0]/git-daemon-export-ok"; } +our $export_auth_hook = undef; + +# only allow viewing of repositories also shown on the overview page +our $strict_export = ""; + +# list of git base URLs used for URL to where fetch project from, +# i.e. full URL is "$git_base_url/$project" +our @git_base_url_list = grep { $_ ne '' } (""); + +# default blob_plain mimetype and default charset for text/plain blob +our $default_blob_plain_mimetype = 'text/plain'; +our $default_text_plain_charset = undef; + +# file to use for guessing MIME types before trying /etc/mime.types +# (relative to the current git repository) +our $mimetypes_file = undef; + +# assume this charset if line contains non-UTF-8 characters; +# it should be valid encoding (see Encoding::Supported(3pm) for list), +# for which encoding all byte sequences are valid, for example +# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it +# could be even 'utf-8' for the old behavior) +our $fallback_encoding = 'latin1'; + +# rename detection options for git-diff and git-diff-tree +# - default is '-M', with the cost proportional to +# (number of removed files) * (number of new files). +# - more costly is '-C' (which implies '-M'), with the cost proportional to +# (number of changed files + number of removed files) * (number of new files) +# - even more costly is '-C', '--find-copies-harder' with cost +# (number of files in the original tree) * (number of new files) +# - one might want to include '-B' option, e.g. '-B', '-M' +our @diff_opts = ('-M'); # taken from git_commit + +# Disables features that would allow repository owners to inject script into +# the gitweb domain. +our $prevent_xss = 0; + +# Path to the highlight executable to use (must be the one from +# http://andre-simon.de/zip/download.php due to assumptions about parameters and output). +# Useful if highlight is not installed on your webserver's PATH. +# [Default: highlight] +our $highlight_bin = "highlight"; + +# information about snapshot formats that gitweb is capable of serving +our %known_snapshot_formats = ( + # name => { + # 'display' => display name, + # 'type' => mime type, + # 'suffix' => filename suffix, + # 'format' => --format for git-archive, + # 'compressor' => [compressor command and arguments] + # (array reference, optional) + # 'disabled' => boolean (optional)} + # + 'tgz' => { + 'display' => 'tar.gz', + 'type' => 'application/x-gzip', + 'suffix' => '.tar.gz', + 'format' => 'tar', + 'compressor' => ['gzip', '-n']}, + + 'tbz2' => { + 'display' => 'tar.bz2', + 'type' => 'application/x-bzip2', + 'suffix' => '.tar.bz2', + 'format' => 'tar', + 'compressor' => ['bzip2']}, + + 'txz' => { + 'display' => 'tar.xz', + 'type' => 'application/x-xz', + 'suffix' => '.tar.xz', + 'format' => 'tar', + 'compressor' => ['xz'], + 'disabled' => 1}, + + 'zip' => { + 'display' => 'zip', + 'type' => 'application/x-zip', + 'suffix' => '.zip', + 'format' => 'zip'}, +); + +# Aliases so we understand old gitweb.snapshot values in repository +# configuration. +our %known_snapshot_format_aliases = ( + 'gzip' => 'tgz', + 'bzip2' => 'tbz2', + 'xz' => 'txz', + + # backward compatibility: legacy gitweb config support + 'x-gzip' => undef, 'gz' => undef, + 'x-bzip2' => undef, 'bz2' => undef, + 'x-zip' => undef, '' => undef, +); + +# Pixel sizes for icons and avatars. If the default font sizes or lineheights +# are changed, it may be appropriate to change these values too via +# $GITWEB_CONFIG. +our %avatar_size = ( + 'default' => 16, + 'double' => 32 +); + +# Used to set the maximum load that we will still respond to gitweb queries. +# If server load exceed this value then return "503 server busy" error. +# If gitweb cannot determined server load, it is taken to be 0. +# Leave it undefined (or set to 'undef') to turn off load checking. +our $maxload = 300; + +# configuration for 'highlight' (http://andre-simon.de/doku/highlight/en/highlight.php) +# match by basename +our %highlight_basename = ( + #'Program' => 'py', + #'Library' => 'py', + 'SConstruct' => 'py', # SCons equivalent of Makefile + 'Makefile' => 'make', +); +# match by extension +our %highlight_ext = ( + # main extensions, defining name of syntax; + # see files in /usr/share/highlight/langDefs/ directory + (map { $_ => $_ } qw(py rb java css js tex bib xml awk bat ini spec tcl sql)), + # alternate extensions, see /etc/highlight/filetypes.conf + (map { $_ => 'c' } qw(c h)), + (map { $_ => 'sh' } qw(sh bash zsh ksh)), + (map { $_ => 'cpp' } qw(cpp cxx c++ cc)), + (map { $_ => 'php' } qw(php php3 php4 php5 phps)), + (map { $_ => 'pl' } qw(pl perl pm)), # perhaps also 'cgi' + (map { $_ => 'make'} qw(make mak mk)), + (map { $_ => 'xml' } qw(xml xhtml html htm)), +); + +# You define site-wide feature defaults here; override them with +# $GITWEB_CONFIG as necessary. +our %feature = ( + # feature => { + # 'sub' => feature-sub (subroutine), + # 'override' => allow-override (boolean), + # 'default' => [ default options...] (array reference)} + # + # if feature is overridable (it means that allow-override has true value), + # then feature-sub will be called with default options as parameters; + # return value of feature-sub indicates if to enable specified feature + # + # if there is no 'sub' key (no feature-sub), then feature cannot be + # overridden + # + # use gitweb_get_feature() to retrieve the value + # (an array) or gitweb_check_feature() to check if + # is enabled + + # Enable the 'blame' blob view, showing the last commit that modified + # each line in the file. This can be very CPU-intensive. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'blame'}{'default'} = [1]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'blame'}{'override'} = 1; + # and in project config gitweb.blame = 0|1; + 'blame' => { + 'sub' => sub { feature_bool('blame', @_) }, + 'override' => 0, + 'default' => [0]}, + + # Enable the 'snapshot' link, providing a compressed archive of any + # tree. This can potentially generate high traffic if you have large + # project. + + # Value is a list of formats defined in %known_snapshot_formats that + # you wish to offer. + # To disable system wide have in $GITWEB_CONFIG + # $feature{'snapshot'}{'default'} = []; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'snapshot'}{'override'} = 1; + # and in project config, a comma-separated list of formats or "none" + # to disable. Example: gitweb.snapshot = tbz2,zip; + 'snapshot' => { + 'sub' => \&feature_snapshot, + 'override' => 0, + 'default' => ['tgz']}, + + # Enable text search, which will list the commits which match author, + # committer or commit text to a given string. Enabled by default. + # Project specific override is not supported. + # + # Note that this controls all search features, which means that if + # it is disabled, then 'grep' and 'pickaxe' search would also be + # disabled. + 'search' => { + 'override' => 0, + 'default' => [1]}, + + # Enable grep search, which will list the files in currently selected + # tree containing the given string. Enabled by default. This can be + # potentially CPU-intensive, of course. + # Note that you need to have 'search' feature enabled too. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'grep'}{'default'} = [1]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'grep'}{'override'} = 1; + # and in project config gitweb.grep = 0|1; + 'grep' => { + 'sub' => sub { feature_bool('grep', @_) }, + 'override' => 0, + 'default' => [1]}, + + # Enable the pickaxe search, which will list the commits that modified + # a given string in a file. This can be practical and quite faster + # alternative to 'blame', but still potentially CPU-intensive. + # Note that you need to have 'search' feature enabled too. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'pickaxe'}{'default'} = [1]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'pickaxe'}{'override'} = 1; + # and in project config gitweb.pickaxe = 0|1; + 'pickaxe' => { + 'sub' => sub { feature_bool('pickaxe', @_) }, + 'override' => 0, + 'default' => [1]}, + + # Enable showing size of blobs in a 'tree' view, in a separate + # column, similar to what 'ls -l' does. This cost a bit of IO. + + # To disable system wide have in $GITWEB_CONFIG + # $feature{'show-sizes'}{'default'} = [0]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'show-sizes'}{'override'} = 1; + # and in project config gitweb.showsizes = 0|1; + 'show-sizes' => { + 'sub' => sub { feature_bool('showsizes', @_) }, + 'override' => 0, + 'default' => [1]}, + + # Make gitweb use an alternative format of the URLs which can be + # more readable and natural-looking: project name is embedded + # directly in the path and the query string contains other + # auxiliary information. All gitweb installations recognize + # URL in either format; this configures in which formats gitweb + # generates links. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'pathinfo'}{'default'} = [1]; + # Project specific override is not supported. + + # Note that you will need to change the default location of CSS, + # favicon, logo and possibly other files to an absolute URL. Also, + # if gitweb.cgi serves as your indexfile, you will need to force + # $my_uri to contain the script name in your $GITWEB_CONFIG. + 'pathinfo' => { + 'override' => 0, + 'default' => [0]}, + + # Make gitweb consider projects in project root subdirectories + # to be forks of existing projects. Given project $projname.git, + # projects matching $projname/*.git will not be shown in the main + # projects list, instead a '+' mark will be added to $projname + # there and a 'forks' view will be enabled for the project, listing + # all the forks. If project list is taken from a file, forks have + # to be listed after the main project. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'forks'}{'default'} = [1]; + # Project specific override is not supported. + 'forks' => { + 'override' => 0, + 'default' => [0]}, + + # Insert custom links to the action bar of all project pages. + # This enables you mainly to link to third-party scripts integrating + # into gitweb; e.g. git-browser for graphical history representation + # or custom web-based repository administration interface. + + # The 'default' value consists of a list of triplets in the form + # (label, link, position) where position is the label after which + # to insert the link and link is a format string where %n expands + # to the project name, %f to the project path within the filesystem, + # %h to the current hash (h gitweb parameter) and %b to the current + # hash base (hb gitweb parameter); %% expands to %. + + # To enable system wide have in $GITWEB_CONFIG e.g. + # $feature{'actions'}{'default'} = [('graphiclog', + # '/git-browser/by-commit.html?r=%n', 'summary')]; + # Project specific override is not supported. + 'actions' => { + 'override' => 0, + 'default' => []}, + + # Allow gitweb scan project content tags of project repository, + # and display the popular Web 2.0-ish "tag cloud" near the projects + # list. Note that this is something COMPLETELY different from the + # normal Git tags. + + # gitweb by itself can show existing tags, but it does not handle + # tagging itself; you need to do it externally, outside gitweb. + # The format is described in git_get_project_ctags() subroutine. + # You may want to install the HTML::TagCloud Perl module to get + # a pretty tag cloud instead of just a list of tags. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'ctags'}{'default'} = [1]; + # Project specific override is not supported. + + # In the future whether ctags editing is enabled might depend + # on the value, but using 1 should always mean no editing of ctags. + 'ctags' => { + 'override' => 0, + 'default' => [0]}, + + # The maximum number of patches in a patchset generated in patch + # view. Set this to 0 or undef to disable patch view, or to a + # negative number to remove any limit. + + # To disable system wide have in $GITWEB_CONFIG + # $feature{'patches'}{'default'} = [0]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'patches'}{'override'} = 1; + # and in project config gitweb.patches = 0|n; + # where n is the maximum number of patches allowed in a patchset. + 'patches' => { + 'sub' => \&feature_patches, + 'override' => 0, + 'default' => [16]}, + + # Avatar support. When this feature is enabled, views such as + # shortlog or commit will display an avatar associated with + # the email of the committer(s) and/or author(s). + + # Currently available providers are gravatar and picon. + # If an unknown provider is specified, the feature is disabled. + + # Picon currently relies on the indiana.edu database. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'avatar'}{'default'} = ['']; + # where is either gravatar or picon. + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'avatar'}{'override'} = 1; + # and in project config gitweb.avatar = ; + 'avatar' => { + 'sub' => \&feature_avatar, + 'override' => 0, + 'default' => ['']}, + + # Enable displaying how much time and how many git commands + # it took to generate and display page. Disabled by default. + # Project specific override is not supported. + 'timed' => { + 'override' => 0, + 'default' => [0]}, + + # Enable turning some links into links to actions which require + # JavaScript to run (like 'blame_incremental'). Not enabled by + # default. Project specific override is currently not supported. + 'javascript-actions' => { + 'override' => 0, + 'default' => [0]}, + + # Enable and configure ability to change common timezone for dates + # in gitweb output via JavaScript. Enabled by default. + # Project specific override is not supported. + 'javascript-timezone' => { + 'override' => 0, + 'default' => [ + 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format, + # or undef to turn off this feature + 'gitweb_tz', # name of cookie where to store selected timezone + 'datetime', # CSS class used to mark up dates for manipulation + ]}, + + # Syntax highlighting support. This is based on Daniel Svensson's + # and Sham Chukoury's work in gitweb-xmms2.git. + # It requires the 'highlight' program present in $PATH, + # and therefore is disabled by default. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'highlight'}{'default'} = [1]; + + 'highlight' => { + 'sub' => sub { feature_bool('highlight', @_) }, + 'override' => 0, + 'default' => [0]}, + + # Enable displaying of remote heads in the heads list + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'remote_heads'}{'default'} = [1]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'remote_heads'}{'override'} = 1; + # and in project config gitweb.remoteheads = 0|1; + 'remote_heads' => { + 'sub' => sub { feature_bool('remote_heads', @_) }, + 'override' => 0, + 'default' => [0]}, + + # Enable showing branches under other refs in addition to heads + + # To set system wide extra branch refs have in $GITWEB_CONFIG + # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice']; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'extra-branch-refs'}{'override'} = 1; + # and in project config gitweb.extrabranchrefs = dirs of choice + # Every directory is separated with whitespace. + + 'extra-branch-refs' => { + 'sub' => \&feature_extra_branch_refs, + 'override' => 0, + 'default' => []}, + + # Redact e-mail addresses. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'email-privacy'}{'default'} = [1]; + 'email-privacy' => { + 'sub' => sub { feature_bool('email-privacy', @_) }, + 'override' => 1, + 'default' => [0]}, +); + +sub gitweb_get_feature { + my ($name) = @_; + return unless exists $feature{$name}; + my ($sub, $override, @defaults) = ( + $feature{$name}{'sub'}, + $feature{$name}{'override'}, + @{$feature{$name}{'default'}}); + # project specific override is possible only if we have project + our $git_dir; # global variable, declared later + if (!$override || !defined $git_dir) { + return @defaults; + } + if (!defined $sub) { + warn "feature $name is not overridable"; + return @defaults; + } + return $sub->(@defaults); +} + +# A wrapper to check if a given feature is enabled. +# With this, you can say +# +# my $bool_feat = gitweb_check_feature('bool_feat'); +# gitweb_check_feature('bool_feat') or somecode; +# +# instead of +# +# my ($bool_feat) = gitweb_get_feature('bool_feat'); +# (gitweb_get_feature('bool_feat'))[0] or somecode; +# +sub gitweb_check_feature { + return (gitweb_get_feature(@_))[0]; +} + + +sub feature_bool { + my $key = shift; + my ($val) = git_get_project_config($key, '--bool'); + + if (!defined $val) { + return ($_[0]); + } elsif ($val eq 'true') { + return (1); + } elsif ($val eq 'false') { + return (0); + } +} + +sub feature_snapshot { + my (@fmts) = @_; + + my ($val) = git_get_project_config('snapshot'); + + if ($val) { + @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val); + } + + return @fmts; +} + +sub feature_patches { + my @val = (git_get_project_config('patches', '--int')); + + if (@val) { + return @val; + } + + return ($_[0]); +} + +sub feature_avatar { + my @val = (git_get_project_config('avatar')); + + return @val ? @val : @_; +} + +sub feature_extra_branch_refs { + my (@branch_refs) = @_; + my $values = git_get_project_config('extrabranchrefs'); + + if ($values) { + $values = config_to_multi ($values); + @branch_refs = (); + foreach my $value (@{$values}) { + push @branch_refs, split /\s+/, $value; + } + } + + return @branch_refs; +} + +# checking HEAD file with -e is fragile if the repository was +# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed +# and then pruned. +sub check_head_link { + my ($dir) = @_; + my $headfile = "$dir/HEAD"; + return ((-e $headfile) || + (-l $headfile && readlink($headfile) =~ /^refs\/heads\//)); +} + +sub check_export_ok { + my ($dir) = @_; + return (check_head_link($dir) && + (!$export_ok || -e "$dir/$export_ok") && + (!$export_auth_hook || $export_auth_hook->($dir))); +} + +# process alternate names for backward compatibility +# filter out unsupported (unknown) snapshot formats +sub filter_snapshot_fmts { + my @fmts = @_; + + @fmts = map { + exists $known_snapshot_format_aliases{$_} ? + $known_snapshot_format_aliases{$_} : $_} @fmts; + @fmts = grep { + exists $known_snapshot_formats{$_} && + !$known_snapshot_formats{$_}{'disabled'}} @fmts; +} + +sub filter_and_validate_refs { + my @refs = @_; + my %unique_refs = (); + + foreach my $ref (@refs) { + die_error(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format($ref)); + # 'heads' are added implicitly in get_branch_refs(). + $unique_refs{$ref} = 1 if ($ref ne 'heads'); + } + return sort keys %unique_refs; +} + +# If it is set to code reference, it is code that it is to be run once per +# request, allowing updating configurations that change with each request, +# while running other code in config file only once. +# +# Otherwise, if it is false then gitweb would process config file only once; +# if it is true then gitweb config would be run for each request. +our $per_request_config = 1; + +# read and parse gitweb config file given by its parameter. +# returns true on success, false on recoverable error, allowing +# to chain this subroutine, using first file that exists. +# dies on errors during parsing config file, as it is unrecoverable. +sub read_config_file { + my $filename = shift; + return unless defined $filename; + if (-e $filename) { + do $filename; + # die if there is a problem accessing the file + die $! if $!; + # die if there are errors parsing config file + die $@ if $@; + return 1; + } + return; +} + +our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON); +sub evaluate_gitweb_config { + our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "gitweb_config.perl"; + our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "/etc/gitweb.conf"; + our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "/etc/gitweb-common.conf"; + + # Protect against duplications of file names, to not read config twice. + # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so + # there possibility of duplication of filename there doesn't matter. + $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON); + $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON); + + # Common system-wide settings for convenience. + # Those settings can be overridden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM. + read_config_file($GITWEB_CONFIG_COMMON); + + # Use first config file that exists. This means use the per-instance + # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG. + read_config_file($GITWEB_CONFIG) and return; + read_config_file($GITWEB_CONFIG_SYSTEM); +} + +# Get loadavg of system, to compare against $maxload. +# Currently it requires '/proc/loadavg' present to get loadavg; +# if it is not present it returns 0, which means no load checking. +sub get_loadavg { + if( -e '/proc/loadavg' ){ + open my $fd, '<', '/proc/loadavg' + or return 0; + my @load = split(/\s+/, scalar <$fd>); + close $fd; + + # The first three columns measure CPU and IO utilization of the last one, + # five, and 10 minute periods. The fourth column shows the number of + # currently running processes and the total number of processes in the m/n + # format. The last column displays the last process ID used. + return $load[0] || 0; + } + # additional checks for load average should go here for things that don't export + # /proc/loadavg + + return 0; +} + +# version of the core git binary +our $git_version; +sub evaluate_git_version { + our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown"; + $number_of_git_cmds++; +} + +sub check_loadavg { + if (defined $maxload && get_loadavg() > $maxload) { + die_error(503, "The load average on the server is too high"); + } +} + +# ====================================================================== +# input validation and dispatch + +# Various hash size-related values. +my $sha1_len = 40; +my $sha256_extra_len = 24; +my $sha256_len = $sha1_len + $sha256_extra_len; + +# A regex matching $len hex characters. $len may be a range (e.g. 7,64). +sub oid_nlen_regex { + my $len = shift; + my $hchr = qr/[0-9a-fA-F]/; + return qr/(?:(?:$hchr){$len})/; +} + +# A regex matching two sets of $nlen hex characters, prefixed by the literal +# string $prefix and with the literal string $infix between them. +sub oid_nlen_prefix_infix_regex { + my $nlen = shift; + my $prefix = shift; + my $infix = shift; + + my $rx = oid_nlen_regex($nlen); + + return qr/^\Q$prefix\E$rx\Q$infix\E$rx$/; +} + +# A regex matching a valid object ID. +our $oid_regex; +{ + my $x = oid_nlen_regex($sha1_len); + my $y = oid_nlen_regex($sha256_extra_len); + $oid_regex = qr/(?:$x(?:$y)?)/; +} + +# input parameters can be collected from a variety of sources (presently, CGI +# and PATH_INFO), so we define an %input_params hash that collects them all +# together during validation: this allows subsequent uses (e.g. href()) to be +# agnostic of the parameter origin + +our %input_params = (); + +# input parameters are stored with the long parameter name as key. This will +# also be used in the href subroutine to convert parameters to their CGI +# equivalent, and since the href() usage is the most frequent one, we store +# the name -> CGI key mapping here, instead of the reverse. +# +# XXX: Warning: If you touch this, check the search form for updating, +# too. + +our @cgi_param_mapping = ( + project => "p", + action => "a", + file_name => "f", + file_parent => "fp", + hash => "h", + hash_parent => "hp", + hash_base => "hb", + hash_parent_base => "hpb", + page => "pg", + order => "o", + searchtext => "s", + searchtype => "st", + snapshot_format => "sf", + extra_options => "opt", + search_use_regexp => "sr", + ctag => "by_tag", + diff_style => "ds", + project_filter => "pf", + # this must be last entry (for manipulation from JavaScript) + javascript => "js" +); +our %cgi_param_mapping = @cgi_param_mapping; + +# we will also need to know the possible actions, for validation +our %actions = ( + "blame" => \&git_blame, + "blame_incremental" => \&git_blame_incremental, + "blame_data" => \&git_blame_data, + "blobdiff" => \&git_blobdiff, + "blobdiff_plain" => \&git_blobdiff_plain, + "blob" => \&git_blob, + "blob_plain" => \&git_blob_plain, + "commitdiff" => \&git_commitdiff, + "commitdiff_plain" => \&git_commitdiff_plain, + "commit" => \&git_commit, + "forks" => \&git_forks, + "heads" => \&git_heads, + "history" => \&git_history, + "log" => \&git_log, + "patch" => \&git_patch, + "patches" => \&git_patches, + "remotes" => \&git_remotes, + "rss" => \&git_rss, + "atom" => \&git_atom, + "search" => \&git_search, + "search_help" => \&git_search_help, + "shortlog" => \&git_shortlog, + "summary" => \&git_summary, + "tag" => \&git_tag, + "tags" => \&git_tags, + "tree" => \&git_tree, + "snapshot" => \&git_snapshot, + "object" => \&git_object, + # those below don't need $project + "opml" => \&git_opml, + "project_list" => \&git_project_list, + "project_index" => \&git_project_index, +); + +# finally, we have the hash of allowed extra_options for the commands that +# allow them +our %allowed_options = ( + "--no-merges" => [ qw(rss atom log shortlog history) ], +); + +# fill %input_params with the CGI parameters. All values except for 'opt' +# should be single values, but opt can be an array. We should probably +# build an array of parameters that can be multi-valued, but since for the time +# being it's only this one, we just single it out +sub evaluate_query_params { + our $cgi; + + while (my ($name, $symbol) = each %cgi_param_mapping) { + if ($symbol eq 'opt') { + $input_params{$name} = [ map { decode_utf8($_) } $cgi->multi_param($symbol) ]; + } else { + $input_params{$name} = decode_utf8($cgi->param($symbol)); + } + } +} + +# now read PATH_INFO and update the parameter list for missing parameters +sub evaluate_path_info { + return if defined $input_params{'project'}; + return if !$path_info; + $path_info =~ s,^/+,,; + return if !$path_info; + + # find which part of PATH_INFO is project + my $project = $path_info; + $project =~ s,/+$,,; + while ($project && !check_head_link("$projectroot/$project")) { + $project =~ s,/*[^/]*$,,; + } + return unless $project; + $input_params{'project'} = $project; + + # do not change any parameters if an action is given using the query string + return if $input_params{'action'}; + $path_info =~ s,^\Q$project\E/*,,; + + # next, check if we have an action + my $action = $path_info; + $action =~ s,/.*$,,; + if (exists $actions{$action}) { + $path_info =~ s,^$action/*,,; + $input_params{'action'} = $action; + } + + # list of actions that want hash_base instead of hash, but can have no + # pathname (f) parameter + my @wants_base = ( + 'tree', + 'history', + ); + + # we want to catch, among others + # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name] + my ($parentrefname, $parentpathname, $refname, $pathname) = + ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/); + + # first, analyze the 'current' part + if (defined $pathname) { + # we got "branch:filename" or "branch:dir/" + # we could use git_get_type(branch:pathname), but: + # - it needs $git_dir + # - it does a git() call + # - the convention of terminating directories with a slash + # makes it superfluous + # - embedding the action in the PATH_INFO would make it even + # more superfluous + $pathname =~ s,^/+,,; + if (!$pathname || substr($pathname, -1) eq "/") { + $input_params{'action'} ||= "tree"; + $pathname =~ s,/$,,; + } else { + # the default action depends on whether we had parent info + # or not + if ($parentrefname) { + $input_params{'action'} ||= "blobdiff_plain"; + } else { + $input_params{'action'} ||= "blob_plain"; + } + } + $input_params{'hash_base'} ||= $refname; + $input_params{'file_name'} ||= $pathname; + } elsif (defined $refname) { + # we got "branch". In this case we have to choose if we have to + # set hash or hash_base. + # + # Most of the actions without a pathname only want hash to be + # set, except for the ones specified in @wants_base that want + # hash_base instead. It should also be noted that hand-crafted + # links having 'history' as an action and no pathname or hash + # set will fail, but that happens regardless of PATH_INFO. + if (defined $parentrefname) { + # if there is parent let the default be 'shortlog' action + # (for http://git.example.com/repo.git/A..B links); if there + # is no parent, dispatch will detect type of object and set + # action appropriately if required (if action is not set) + $input_params{'action'} ||= "shortlog"; + } + if ($input_params{'action'} && + grep { $_ eq $input_params{'action'} } @wants_base) { + $input_params{'hash_base'} ||= $refname; + } else { + $input_params{'hash'} ||= $refname; + } + } + + # next, handle the 'parent' part, if present + if (defined $parentrefname) { + # a missing pathspec defaults to the 'current' filename, allowing e.g. + # someproject/blobdiff/oldrev..newrev:/filename + if ($parentpathname) { + $parentpathname =~ s,^/+,,; + $parentpathname =~ s,/$,,; + $input_params{'file_parent'} ||= $parentpathname; + } else { + $input_params{'file_parent'} ||= $input_params{'file_name'}; + } + # we assume that hash_parent_base is wanted if a path was specified, + # or if the action wants hash_base instead of hash + if (defined $input_params{'file_parent'} || + grep { $_ eq $input_params{'action'} } @wants_base) { + $input_params{'hash_parent_base'} ||= $parentrefname; + } else { + $input_params{'hash_parent'} ||= $parentrefname; + } + } + + # for the snapshot action, we allow URLs in the form + # $project/snapshot/$hash.ext + # where .ext determines the snapshot and gets removed from the + # passed $refname to provide the $hash. + # + # To be able to tell that $refname includes the format extension, we + # require the following two conditions to be satisfied: + # - the hash input parameter MUST have been set from the $refname part + # of the URL (i.e. they must be equal) + # - the snapshot format MUST NOT have been defined already (e.g. from + # CGI parameter sf) + # It's also useless to try any matching unless $refname has a dot, + # so we check for that too + if (defined $input_params{'action'} && + $input_params{'action'} eq 'snapshot' && + defined $refname && index($refname, '.') != -1 && + $refname eq $input_params{'hash'} && + !defined $input_params{'snapshot_format'}) { + # We loop over the known snapshot formats, checking for + # extensions. Allowed extensions are both the defined suffix + # (which includes the initial dot already) and the snapshot + # format key itself, with a prepended dot + while (my ($fmt, $opt) = each %known_snapshot_formats) { + my $hash = $refname; + unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) { + next; + } + my $sfx = $1; + # a valid suffix was found, so set the snapshot format + # and reset the hash parameter + $input_params{'snapshot_format'} = $fmt; + $input_params{'hash'} = $hash; + # we also set the format suffix to the one requested + # in the URL: this way a request for e.g. .tgz returns + # a .tgz instead of a .tar.gz + $known_snapshot_formats{$fmt}{'suffix'} = $sfx; + last; + } + } +} + +our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base, + $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp, + $searchtext, $search_regexp, $project_filter); +sub evaluate_and_validate_params { + our $action = $input_params{'action'}; + if (defined $action) { + if (!is_valid_action($action)) { + die_error(400, "Invalid action parameter"); + } + } + + # parameters which are pathnames + our $project = $input_params{'project'}; + if (defined $project) { + if (!is_valid_project($project)) { + undef $project; + die_error(404, "No such project"); + } + } + + our $project_filter = $input_params{'project_filter'}; + if (defined $project_filter) { + if (!is_valid_pathname($project_filter)) { + die_error(404, "Invalid project_filter parameter"); + } + } + + our $file_name = $input_params{'file_name'}; + if (defined $file_name) { + if (!is_valid_pathname($file_name)) { + die_error(400, "Invalid file parameter"); + } + } + + our $file_parent = $input_params{'file_parent'}; + if (defined $file_parent) { + if (!is_valid_pathname($file_parent)) { + die_error(400, "Invalid file parent parameter"); + } + } + + # parameters which are refnames + our $hash = $input_params{'hash'}; + if (defined $hash) { + if (!is_valid_refname($hash)) { + die_error(400, "Invalid hash parameter"); + } + } + + our $hash_parent = $input_params{'hash_parent'}; + if (defined $hash_parent) { + if (!is_valid_refname($hash_parent)) { + die_error(400, "Invalid hash parent parameter"); + } + } + + our $hash_base = $input_params{'hash_base'}; + if (defined $hash_base) { + if (!is_valid_refname($hash_base)) { + die_error(400, "Invalid hash base parameter"); + } + } + + our @extra_options = @{$input_params{'extra_options'}}; + # @extra_options is always defined, since it can only be (currently) set from + # CGI, and $cgi->param() returns the empty array in array context if the param + # is not set + foreach my $opt (@extra_options) { + if (not exists $allowed_options{$opt}) { + die_error(400, "Invalid option parameter"); + } + if (not grep(/^$action$/, @{$allowed_options{$opt}})) { + die_error(400, "Invalid option parameter for this action"); + } + } + + our $hash_parent_base = $input_params{'hash_parent_base'}; + if (defined $hash_parent_base) { + if (!is_valid_refname($hash_parent_base)) { + die_error(400, "Invalid hash parent base parameter"); + } + } + + # other parameters + our $page = $input_params{'page'}; + if (defined $page) { + if ($page =~ m/[^0-9]/) { + die_error(400, "Invalid page parameter"); + } + } + + our $searchtype = $input_params{'searchtype'}; + if (defined $searchtype) { + if ($searchtype =~ m/[^a-z]/) { + die_error(400, "Invalid searchtype parameter"); + } + } + + our $search_use_regexp = $input_params{'search_use_regexp'}; + + our $searchtext = $input_params{'searchtext'}; + our $search_regexp = undef; + if (defined $searchtext) { + if (length($searchtext) < 2) { + die_error(403, "At least two characters are required for search parameter"); + } + if ($search_use_regexp) { + $search_regexp = $searchtext; + if (!eval { qr/$search_regexp/; 1; }) { + (my $error = $@) =~ s/ at \S+ line \d+.*\n?//; + die_error(400, "Invalid search regexp '$search_regexp'", + esc_html($error)); + } + } else { + $search_regexp = quotemeta $searchtext; + } + } +} + +# path to the current git repository +our $git_dir; +sub evaluate_git_dir { + our $git_dir = "$projectroot/$project" if $project; +} + +our (@snapshot_fmts, $git_avatar, @extra_branch_refs); +sub configure_gitweb_features { + # list of supported snapshot formats + our @snapshot_fmts = gitweb_get_feature('snapshot'); + @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts); + + our ($git_avatar) = gitweb_get_feature('avatar'); + $git_avatar = '' unless $git_avatar =~ /^(?:gravatar|picon)$/s; + + our @extra_branch_refs = gitweb_get_feature('extra-branch-refs'); + @extra_branch_refs = filter_and_validate_refs (@extra_branch_refs); +} + +sub get_branch_refs { + return ('heads', @extra_branch_refs); +} + +# custom error handler: 'die ' is Internal Server Error +sub handle_errors_html { + my $msg = shift; # it is already HTML escaped + + # to avoid infinite loop where error occurs in die_error, + # change handler to default handler, disabling handle_errors_html + set_message("Error occurred when inside die_error:\n$msg"); + + # you cannot jump out of die_error when called as error handler; + # the subroutine set via CGI::Carp::set_message is called _after_ + # HTTP headers are already written, so it cannot write them itself + die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1); +} +set_message(\&handle_errors_html); + +# dispatch +sub dispatch { + if (!defined $action) { + if (defined $hash) { + $action = git_get_type($hash); + $action or die_error(404, "Object does not exist"); + } elsif (defined $hash_base && defined $file_name) { + $action = git_get_type("$hash_base:$file_name"); + $action or die_error(404, "File or directory does not exist"); + } elsif (defined $project) { + $action = 'summary'; + } else { + $action = 'project_list'; + } + } + if (!defined($actions{$action})) { + die_error(400, "Unknown action"); + } + if ($action !~ m/^(?:opml|project_list|project_index)$/ && + !$project) { + die_error(400, "Project needed"); + } + $actions{$action}->(); +} + +sub reset_timer { + our $t0 = [ gettimeofday() ] + if defined $t0; + our $number_of_git_cmds = 0; +} + +our $first_request = 1; +sub run_request { + reset_timer(); + + evaluate_uri(); + if ($first_request) { + evaluate_gitweb_config(); + evaluate_git_version(); + } + if ($per_request_config) { + if (ref($per_request_config) eq 'CODE') { + $per_request_config->(); + } elsif (!$first_request) { + evaluate_gitweb_config(); + } + } + check_loadavg(); + + # $projectroot and $projects_list might be set in gitweb config file + $projects_list ||= $projectroot; + + evaluate_query_params(); + evaluate_path_info(); + evaluate_and_validate_params(); + evaluate_git_dir(); + + configure_gitweb_features(); + + dispatch(); +} + +our $is_last_request = sub { 1 }; +our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook); +our $CGI = 'CGI'; +our $cgi; +our $FCGI_Stream_PRINT_raw = \&FCGI::Stream::PRINT; +sub configure_as_fcgi { + require CGI::Fast; + our $CGI = 'CGI::Fast'; + # FCGI is not Unicode aware hence the UTF-8 encoding must be done manually. + # However no encoding must be done within git_blob_plain() and git_snapshot() + # which must still output in raw binary mode. + no warnings 'redefine'; + my $enc = Encode::find_encoding('UTF-8'); + *FCGI::Stream::PRINT = sub { + my @OUTPUT = @_; + for (my $i = 1; $i < @_; $i++) { + $OUTPUT[$i] = $enc->encode($_[$i], Encode::FB_CROAK|Encode::LEAVE_SRC); + } + @_ = @OUTPUT; + goto $FCGI_Stream_PRINT_raw; + }; + + my $request_number = 0; + # let each child service 100 requests + our $is_last_request = sub { ++$request_number > 100 }; +} +sub evaluate_argv { + my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__; + configure_as_fcgi() + if $script_name =~ /\.fcgi$/; + + return unless (@ARGV); + + require Getopt::Long; + Getopt::Long::GetOptions( + 'fastcgi|fcgi|f' => \&configure_as_fcgi, + 'nproc|n=i' => sub { + my ($arg, $val) = @_; + return unless eval { require FCGI::ProcManager; 1; }; + my $proc_manager = FCGI::ProcManager->new({ + n_processes => $val, + }); + our $pre_listen_hook = sub { $proc_manager->pm_manage() }; + our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() }; + our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() }; + }, + ); +} + +sub run { + evaluate_argv(); + + $first_request = 1; + $pre_listen_hook->() + if $pre_listen_hook; + + REQUEST: + while ($cgi = $CGI->new()) { + $pre_dispatch_hook->() + if $pre_dispatch_hook; + + run_request(); + + $post_dispatch_hook->() + if $post_dispatch_hook; + $first_request = 0; + + last REQUEST if ($is_last_request->()); + } + + DONE_GITWEB: + 1; +} + +run(); + +if (defined caller) { + # wrapped in a subroutine processing requests, + # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI + return; +} else { + # pure CGI script, serving single request + exit; +} + +## ====================================================================== +## action links + +# possible values of extra options +# -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base) +# -replay => 1 - start from a current view (replay with modifications) +# -path_info => 0|1 - don't use/use path_info URL (if possible) +# -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone +sub href { + my %params = @_; + # default is to use -absolute url() i.e. $my_uri + my $href = $params{-full} ? $my_url : $my_uri; + + # implicit -replay, must be first of implicit params + $params{-replay} = 1 if (keys %params == 1 && $params{-anchor}); + + $params{'project'} = $project unless exists $params{'project'}; + + if ($params{-replay}) { + while (my ($name, $symbol) = each %cgi_param_mapping) { + if (!exists $params{$name}) { + $params{$name} = $input_params{$name}; + } + } + } + + my $use_pathinfo = gitweb_check_feature('pathinfo'); + if (defined $params{'project'} && + (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) { + # try to put as many parameters as possible in PATH_INFO: + # - project name + # - action + # - hash_parent or hash_parent_base:/file_parent + # - hash or hash_base:/filename + # - the snapshot_format as an appropriate suffix + + # When the script is the root DirectoryIndex for the domain, + # $href here would be something like http://gitweb.example.com/ + # Thus, we strip any trailing / from $href, to spare us double + # slashes in the final URL + $href =~ s,/$,,; + + # Then add the project name, if present + $href .= "/".esc_path_info($params{'project'}); + delete $params{'project'}; + + # since we destructively absorb parameters, we keep this + # boolean that remembers if we're handling a snapshot + my $is_snapshot = $params{'action'} eq 'snapshot'; + + # Summary just uses the project path URL, any other action is + # added to the URL + if (defined $params{'action'}) { + $href .= "/".esc_path_info($params{'action'}) + unless $params{'action'} eq 'summary'; + delete $params{'action'}; + } + + # Next, we put hash_parent_base:/file_parent..hash_base:/file_name, + # stripping nonexistent or useless pieces + $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'} + || $params{'hash_parent'} || $params{'hash'}); + if (defined $params{'hash_base'}) { + if (defined $params{'hash_parent_base'}) { + $href .= esc_path_info($params{'hash_parent_base'}); + # skip the file_parent if it's the same as the file_name + if (defined $params{'file_parent'}) { + if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) { + delete $params{'file_parent'}; + } elsif ($params{'file_parent'} !~ /\.\./) { + $href .= ":/".esc_path_info($params{'file_parent'}); + delete $params{'file_parent'}; + } + } + $href .= ".."; + delete $params{'hash_parent'}; + delete $params{'hash_parent_base'}; + } elsif (defined $params{'hash_parent'}) { + $href .= esc_path_info($params{'hash_parent'}). ".."; + delete $params{'hash_parent'}; + } + + $href .= esc_path_info($params{'hash_base'}); + if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) { + $href .= ":/".esc_path_info($params{'file_name'}); + delete $params{'file_name'}; + } + delete $params{'hash'}; + delete $params{'hash_base'}; + } elsif (defined $params{'hash'}) { + $href .= esc_path_info($params{'hash'}); + delete $params{'hash'}; + } + + # If the action was a snapshot, we can absorb the + # snapshot_format parameter too + if ($is_snapshot) { + my $fmt = $params{'snapshot_format'}; + # snapshot_format should always be defined when href() + # is called, but just in case some code forgets, we + # fall back to the default + $fmt ||= $snapshot_fmts[0]; + $href .= $known_snapshot_formats{$fmt}{'suffix'}; + delete $params{'snapshot_format'}; + } + } + + # now encode the parameters explicitly + my @result = (); + for (my $i = 0; $i < @cgi_param_mapping; $i += 2) { + my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]); + if (defined $params{$name}) { + if (ref($params{$name}) eq "ARRAY") { + foreach my $par (@{$params{$name}}) { + push @result, $symbol . "=" . esc_param($par); + } + } else { + push @result, $symbol . "=" . esc_param($params{$name}); + } + } + } + $href .= "?" . join(';', @result) if scalar @result; + + # final transformation: trailing spaces must be escaped (URI-encoded) + $href =~ s/(\s+)$/CGI::escape($1)/e; + + if ($params{-anchor}) { + $href .= "#".esc_param($params{-anchor}); + } + + return $href; +} + + +## ====================================================================== +## validation, quoting/unquoting and escaping + +sub is_valid_action { + my $input = shift; + return undef unless exists $actions{$input}; + return 1; +} + +sub is_valid_project { + my $input = shift; + + return unless defined $input; + if (!is_valid_pathname($input) || + !(-d "$projectroot/$input") || + !check_export_ok("$projectroot/$input") || + ($strict_export && !project_in_list($input))) { + return undef; + } else { + return 1; + } +} + +sub is_valid_pathname { + my $input = shift; + + return undef unless defined $input; + # no '.' or '..' as elements of path, i.e. no '.' or '..' + # at the beginning, at the end, and between slashes. + # also this catches doubled slashes + if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) { + return undef; + } + # no null characters + if ($input =~ m!\0!) { + return undef; + } + return 1; +} + +sub is_valid_ref_format { + my $input = shift; + + return undef unless defined $input; + # restrictions on ref name according to git-check-ref-format + if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) { + return undef; + } + return 1; +} + +sub is_valid_refname { + my $input = shift; + + return undef unless defined $input; + # textual hashes are O.K. + if ($input =~ m/^$oid_regex$/) { + return 1; + } + # it must be correct pathname + is_valid_pathname($input) or return undef; + # check git-check-ref-format restrictions + is_valid_ref_format($input) or return undef; + return 1; +} + +# decode sequences of octets in utf8 into Perl's internal form, +# which is utf-8 with utf8 flag set if needed. gitweb writes out +# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning +sub to_utf8 { + my $str = shift; + return undef unless defined $str; + + if (utf8::is_utf8($str) || utf8::decode($str)) { + return $str; + } else { + return decode($fallback_encoding, $str, Encode::FB_DEFAULT); + } +} + +# quote unsafe chars, but keep the slash, even when it's not +# correct, but quoted slashes look too horrible in bookmarks +sub esc_param { + my $str = shift; + return undef unless defined $str; + $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg; + $str =~ s/ /\+/g; + return $str; +} + +# the quoting rules for path_info fragment are slightly different +sub esc_path_info { + my $str = shift; + return undef unless defined $str; + + # path_info doesn't treat '+' as space (specially), but '?' must be escaped + $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg; + + return $str; +} + +# quote unsafe chars in whole URL, so some characters cannot be quoted +sub esc_url { + my $str = shift; + return undef unless defined $str; + $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg; + $str =~ s/ /\+/g; + return $str; +} + +# quote unsafe characters in HTML attributes +sub esc_attr { + + # for XHTML conformance escaping '"' to '"' is not enough + return esc_html(@_); +} + +# replace invalid utf8 character with SUBSTITUTION sequence +sub esc_html { + my $str = shift; + my %opts = @_; + + return undef unless defined $str; + + $str = to_utf8($str); + $str = $cgi->escapeHTML($str); + if ($opts{'-nbsp'}) { + $str =~ s/ / /g; + } + $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg; + return $str; +} + +# quote control characters and escape filename to HTML +sub esc_path { + my $str = shift; + my %opts = @_; + + return undef unless defined $str; + + $str = to_utf8($str); + $str = $cgi->escapeHTML($str); + if ($opts{'-nbsp'}) { + $str =~ s/ / /g; + } + $str =~ s|([[:cntrl:]])|quot_cec($1)|eg; + return $str; +} + +# Sanitize for use in XHTML + application/xml+xhtml (valid XML 1.0) +sub sanitize { + my $str = shift; + + return undef unless defined $str; + + $str = to_utf8($str); + $str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg; + return $str; +} + +# Make control characters "printable", using character escape codes (CEC) +sub quot_cec { + my $cntrl = shift; + my %opts = @_; + my %es = ( # character escape codes, aka escape sequences + "\t" => '\t', # tab (HT) + "\n" => '\n', # line feed (LF) + "\r" => '\r', # carriage return (CR) + "\f" => '\f', # form feed (FF) + "\b" => '\b', # backspace (BS) + "\a" => '\a', # alarm (bell) (BEL) + "\e" => '\e', # escape (ESC) + "\013" => '\v', # vertical tab (VT) + "\000" => '\0', # nul character (NUL) + ); + my $chr = ( (exists $es{$cntrl}) + ? $es{$cntrl} + : sprintf('\%2x', ord($cntrl)) ); + if ($opts{-nohtml}) { + return $chr; + } else { + return "$chr"; + } +} + +# Alternatively use unicode control pictures codepoints, +# Unicode "printable representation" (PR) +sub quot_upr { + my $cntrl = shift; + my %opts = @_; + + my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl)); + if ($opts{-nohtml}) { + return $chr; + } else { + return "$chr"; + } +} + +# git may return quoted and escaped filenames +sub unquote { + my $str = shift; + + sub unq { + my $seq = shift; + my %es = ( # character escape codes, aka escape sequences + 't' => "\t", # tab (HT, TAB) + 'n' => "\n", # newline (NL) + 'r' => "\r", # return (CR) + 'f' => "\f", # form feed (FF) + 'b' => "\b", # backspace (BS) + 'a' => "\a", # alarm (bell) (BEL) + 'e' => "\e", # escape (ESC) + 'v' => "\013", # vertical tab (VT) + ); + + if ($seq =~ m/^[0-7]{1,3}$/) { + # octal char sequence + return chr(oct($seq)); + } elsif (exists $es{$seq}) { + # C escape sequence, aka character escape code + return $es{$seq}; + } + # quoted ordinary character + return $seq; + } + + if ($str =~ m/^"(.*)"$/) { + # needs unquoting + $str = $1; + $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg; + } + return $str; +} + +# escape tabs (convert tabs to spaces) +sub untabify { + my $line = shift; + + while ((my $pos = index($line, "\t")) != -1) { + if (my $count = (8 - ($pos % 8))) { + my $spaces = ' ' x $count; + $line =~ s/\t/$spaces/; + } + } + + return $line; +} + +sub project_in_list { + my $project = shift; + my @list = git_get_projects_list(); + return @list && scalar(grep { $_->{'path'} eq $project } @list); +} + +## ---------------------------------------------------------------------- +## HTML aware string manipulation + +# Try to chop given string on a word boundary between position +# $len and $len+$add_len. If there is no word boundary there, +# chop at $len+$add_len. Do not chop if chopped part plus ellipsis +# (marking chopped part) would be longer than given string. +sub chop_str { + my $str = shift; + my $len = shift; + my $add_len = shift || 10; + my $where = shift || 'right'; # 'left' | 'center' | 'right' + + # Make sure perl knows it is utf8 encoded so we don't + # cut in the middle of a utf8 multibyte char. + $str = to_utf8($str); + + # allow only $len chars, but don't cut a word if it would fit in $add_len + # if it doesn't fit, cut it if it's still longer than the dots we would add + # remove chopped character entities entirely + + # when chopping in the middle, distribute $len into left and right part + # return early if chopping wouldn't make string shorter + if ($where eq 'center') { + return $str if ($len + 5 >= length($str)); # filler is length 5 + $len = int($len/2); + } else { + return $str if ($len + 4 >= length($str)); # filler is length 4 + } + + # regexps: ending and beginning with word part up to $add_len + my $endre = qr/.{$len}\w{0,$add_len}/; + my $begre = qr/\w{0,$add_len}.{$len}/; + + if ($where eq 'left') { + $str =~ m/^(.*?)($begre)$/; + my ($lead, $body) = ($1, $2); + if (length($lead) > 4) { + $lead = " ..."; + } + return "$lead$body"; + + } elsif ($where eq 'center') { + $str =~ m/^($endre)(.*)$/; + my ($left, $str) = ($1, $2); + $str =~ m/^(.*?)($begre)$/; + my ($mid, $right) = ($1, $2); + if (length($mid) > 5) { + $mid = " ... "; + } + return "$left$mid$right"; + + } else { + $str =~ m/^($endre)(.*)$/; + my $body = $1; + my $tail = $2; + if (length($tail) > 4) { + $tail = "... "; + } + return "$body$tail"; + } +} + +# takes the same arguments as chop_str, but also wraps a around the +# result with a title attribute if it does get chopped. Additionally, the +# string is HTML-escaped. +sub chop_and_escape_str { + my ($str) = @_; + + my $chopped = chop_str(@_); + $str = to_utf8($str); + if ($chopped eq $str) { + return esc_html($chopped); + } else { + $str =~ s/[[:cntrl:]]/?/g; + return $cgi->span({-title=>$str}, esc_html($chopped)); + } +} + +# Highlight selected fragments of string, using given CSS class, +# and escape HTML. It is assumed that fragments do not overlap. +# Regions are passed as list of pairs (array references). +# +# Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns +# 'foobar' +sub esc_html_hl_regions { + my ($str, $css_class, @sel) = @_; + my %opts = grep { ref($_) ne 'ARRAY' } @sel; + @sel = grep { ref($_) eq 'ARRAY' } @sel; + return esc_html($str, %opts) unless @sel; + + my $out = ''; + my $pos = 0; + + for my $s (@sel) { + my ($begin, $end) = @$s; + + # Don't create empty elements. + next if $end <= $begin; + + my $escaped = esc_html(substr($str, $begin, $end - $begin), + %opts); + + $out .= esc_html(substr($str, $pos, $begin - $pos), %opts) + if ($begin - $pos > 0); + $out .= $cgi->span({-class => $css_class}, $escaped); + + $pos = $end; + } + $out .= esc_html(substr($str, $pos), %opts) + if ($pos < length($str)); + + return $out; +} + +# return positions of beginning and end of each match +sub matchpos_list { + my ($str, $regexp) = @_; + return unless (defined $str && defined $regexp); + + my @matches; + while ($str =~ /$regexp/g) { + push @matches, [$-[0], $+[0]]; + } + return @matches; +} + +# highlight match (if any), and escape HTML +sub esc_html_match_hl { + my ($str, $regexp) = @_; + return esc_html($str) unless defined $regexp; + + my @matches = matchpos_list($str, $regexp); + return esc_html($str) unless @matches; + + return esc_html_hl_regions($str, 'match', @matches); +} + + +# highlight match (if any) of shortened string, and escape HTML +sub esc_html_match_hl_chopped { + my ($str, $chopped, $regexp) = @_; + return esc_html_match_hl($str, $regexp) unless defined $chopped; + + my @matches = matchpos_list($str, $regexp); + return esc_html($chopped) unless @matches; + + # filter matches so that we mark chopped string + my $tail = "... "; # see chop_str + unless ($chopped =~ s/\Q$tail\E$//) { + $tail = ''; + } + my $chop_len = length($chopped); + my $tail_len = length($tail); + my @filtered; + + for my $m (@matches) { + if ($m->[0] > $chop_len) { + push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0); + last; + } elsif ($m->[1] > $chop_len) { + push @filtered, [ $m->[0], $chop_len + $tail_len ]; + last; + } + push @filtered, $m; + } + + return esc_html_hl_regions($chopped . $tail, 'match', @filtered); +} + +## ---------------------------------------------------------------------- +## functions returning short strings + +# CSS class for given age value (in seconds) +sub age_class { + my $age = shift; + + if (!defined $age) { + return "noage"; + } elsif ($age < 60*60*2) { + return "age0"; + } elsif ($age < 60*60*24*2) { + return "age1"; + } else { + return "age2"; + } +} + +# convert age in seconds to "nn units ago" string +sub age_string { + my $age = shift; + my $age_str; + + if ($age > 60*60*24*365*2) { + $age_str = (int $age/60/60/24/365); + $age_str .= " years ago"; + } elsif ($age > 60*60*24*(365/12)*2) { + $age_str = int $age/60/60/24/(365/12); + $age_str .= " months ago"; + } elsif ($age > 60*60*24*7*2) { + $age_str = int $age/60/60/24/7; + $age_str .= " weeks ago"; + } elsif ($age > 60*60*24*2) { + $age_str = int $age/60/60/24; + $age_str .= " days ago"; + } elsif ($age > 60*60*2) { + $age_str = int $age/60/60; + $age_str .= " hours ago"; + } elsif ($age > 60*2) { + $age_str = int $age/60; + $age_str .= " min ago"; + } elsif ($age > 2) { + $age_str = int $age; + $age_str .= " sec ago"; + } else { + $age_str .= " right now"; + } + return $age_str; +} + +use constant { + S_IFINVALID => 0030000, + S_IFGITLINK => 0160000, +}; + +# submodule/subproject, a commit object reference +sub S_ISGITLINK { + my $mode = shift; + + return (($mode & S_IFMT) == S_IFGITLINK) +} + +# convert file mode in octal to symbolic file mode string +sub mode_str { + my $mode = oct shift; + + if (S_ISGITLINK($mode)) { + return 'm---------'; + } elsif (S_ISDIR($mode & S_IFMT)) { + return 'drwxr-xr-x'; + } elsif (S_ISLNK($mode)) { + return 'lrwxrwxrwx'; + } elsif (S_ISREG($mode)) { + # git cares only about the executable bit + if ($mode & S_IXUSR) { + return '-rwxr-xr-x'; + } else { + return '-rw-r--r--'; + }; + } else { + return '----------'; + } +} + +# convert file mode in octal to file type string +sub file_type { + my $mode = shift; + + if ($mode !~ m/^[0-7]+$/) { + return $mode; + } else { + $mode = oct $mode; + } + + if (S_ISGITLINK($mode)) { + return "submodule"; + } elsif (S_ISDIR($mode & S_IFMT)) { + return "directory"; + } elsif (S_ISLNK($mode)) { + return "symlink"; + } elsif (S_ISREG($mode)) { + return "file"; + } else { + return "unknown"; + } +} + +# convert file mode in octal to file type description string +sub file_type_long { + my $mode = shift; + + if ($mode !~ m/^[0-7]+$/) { + return $mode; + } else { + $mode = oct $mode; + } + + if (S_ISGITLINK($mode)) { + return "submodule"; + } elsif (S_ISDIR($mode & S_IFMT)) { + return "directory"; + } elsif (S_ISLNK($mode)) { + return "symlink"; + } elsif (S_ISREG($mode)) { + if ($mode & S_IXUSR) { + return "executable"; + } else { + return "file"; + }; + } else { + return "unknown"; + } +} + + +## ---------------------------------------------------------------------- +## functions returning short HTML fragments, or transforming HTML fragments +## which don't belong to other sections + +# format line of commit message. +sub format_log_line_html { + my $line = shift; + + # Potentially abbreviated OID. + my $regex = oid_nlen_regex("7,64"); + + $line = esc_html($line, -nbsp=>1); + $line =~ s{ + \b + ( + # The output of "git describe", e.g. v2.10.0-297-gf6727b0 + # or hadoop-20160921-113441-20-g094fb7d + (?a({-href => href(action=>"object", hash=>$1), + -class => "text"}, $1); + }egx; + + return $line; +} + +# format marker of refs pointing to given object + +# the destination action is chosen based on object type and current context: +# - for annotated tags, we choose the tag view unless it's the current view +# already, in which case we go to shortlog view +# - for other refs, we keep the current view if we're in history, shortlog or +# log view, and select shortlog otherwise +sub format_ref_marker { + my ($refs, $id) = @_; + my $markers = ''; + + if (defined $refs->{$id}) { + foreach my $ref (@{$refs->{$id}}) { + # this code exploits the fact that non-lightweight tags are the + # only indirect objects, and that they are the only objects for which + # we want to use tag instead of shortlog as action + my ($type, $name) = qw(); + my $indirect = ($ref =~ s/\^\{\}$//); + # e.g. tags/v2.6.11 or heads/next + if ($ref =~ m!^(.*?)s?/(.*)$!) { + $type = $1; + $name = $2; + } else { + $type = "ref"; + $name = $ref; + } + + my $class = $type; + $class .= " indirect" if $indirect; + + my $dest_action = "shortlog"; + + if ($indirect) { + $dest_action = "tag" unless $action eq "tag"; + } elsif ($action =~ /^(history|(short)?log)$/) { + $dest_action = $action; + } + + my $dest = ""; + $dest .= "refs/" unless $ref =~ m!^refs/!; + $dest .= $ref; + + my $link = $cgi->a({ + -href => href( + action=>$dest_action, + hash=>$dest + )}, esc_html($name)); + + $markers .= " " . + $link . ""; + } + } + + if ($markers) { + return ' '. $markers . ''; + } else { + return ""; + } +} + +# format, perhaps shortened and with markers, title line +sub format_subject_html { + my ($long, $short, $href, $extra) = @_; + $extra = '' unless defined($extra); + + if (length($short) < length($long)) { + $long =~ s/[[:cntrl:]]/?/g; + return $cgi->a({-href => $href, -class => "list subject", + -title => to_utf8($long)}, + esc_html($short)) . $extra; + } else { + return $cgi->a({-href => $href, -class => "list subject"}, + esc_html($long)) . $extra; + } +} + +# Rather than recomputing the url for an email multiple times, we cache it +# after the first hit. This gives a visible benefit in views where the avatar +# for the same email is used repeatedly (e.g. shortlog). +# The cache is shared by all avatar engines (currently gravatar only), which +# are free to use it as preferred. Since only one avatar engine is used for any +# given page, there's no risk for cache conflicts. +our %avatar_cache = (); + +# Compute the picon url for a given email, by using the picon search service over at +# http://www.cs.indiana.edu/picons/search.html +sub picon_url { + my $email = lc shift; + if (!$avatar_cache{$email}) { + my ($user, $domain) = split('@', $email); + $avatar_cache{$email} = + "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" . + "$domain/$user/" . + "users+domains+unknown/up/single"; + } + return $avatar_cache{$email}; +} + +# Compute the gravatar url for a given email, if it's not in the cache already. +# Gravatar stores only the part of the URL before the size, since that's the +# one computationally more expensive. This also allows reuse of the cache for +# different sizes (for this particular engine). +sub gravatar_url { + my $email = lc shift; + my $size = shift; + $avatar_cache{$email} ||= + "//www.gravatar.com/avatar/" . + md5_hex($email) . "?s="; + return $avatar_cache{$email} . $size; +} + +# Insert an avatar for the given $email at the given $size if the feature +# is enabled. +sub git_get_avatar { + my ($email, %opts) = @_; + my $pre_white = ($opts{-pad_before} ? " " : ""); + my $post_white = ($opts{-pad_after} ? " " : ""); + $opts{-size} ||= 'default'; + my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'}; + my $url = ""; + if ($git_avatar eq 'gravatar') { + $url = gravatar_url($email, $size); + } elsif ($git_avatar eq 'picon') { + $url = picon_url($email); + } + # Other providers can be added by extending the if chain, defining $url + # as needed. If no variant puts something in $url, we assume avatars + # are completely disabled/unavailable. + if ($url) { + return $pre_white . + "" . $post_white; + } else { + return ""; + } +} + +sub format_search_author { + my ($author, $searchtype, $displaytext) = @_; + my $have_search = gitweb_check_feature('search'); + + if ($have_search) { + my $performed = ""; + if ($searchtype eq 'author') { + $performed = "authored"; + } elsif ($searchtype eq 'committer') { + $performed = "committed"; + } + + return $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$author, + searchtype=>$searchtype), class=>"list", + title=>"Search for commits $performed by $author"}, + $displaytext); + + } else { + return $displaytext; + } +} + +# format the author name of the given commit with the given tag +# the author name is chopped and escaped according to the other +# optional parameters (see chop_str). +sub format_author_html { + my $tag = shift; + my $co = shift; + my $author = chop_and_escape_str($co->{'author_name'}, @_); + return "<$tag class=\"author\">" . + format_search_author($co->{'author_name'}, "author", + git_get_avatar($co->{'author_email'}, -pad_after => 1) . + $author) . + ""; +} + +# format git diff header line, i.e. "diff --(git|combined|cc) ..." +sub format_git_diff_header_line { + my $line = shift; + my $diffinfo = shift; + my ($from, $to) = @_; + + if ($diffinfo->{'nparents'}) { + # combined diff + $line =~ s!^(diff (.*?) )"?.*$!$1!; + if ($to->{'href'}) { + $line .= $cgi->a({-href => $to->{'href'}, -class => "path"}, + esc_path($to->{'file'})); + } else { # file was deleted (no href) + $line .= esc_path($to->{'file'}); + } + } else { + # "ordinary" diff + $line =~ s!^(diff (.*?) )"?a/.*$!$1!; + if ($from->{'href'}) { + $line .= $cgi->a({-href => $from->{'href'}, -class => "path"}, + 'a/' . esc_path($from->{'file'})); + } else { # file was added (no href) + $line .= 'a/' . esc_path($from->{'file'}); + } + $line .= ' '; + if ($to->{'href'}) { + $line .= $cgi->a({-href => $to->{'href'}, -class => "path"}, + 'b/' . esc_path($to->{'file'})); + } else { # file was deleted + $line .= 'b/' . esc_path($to->{'file'}); + } + } + + return "
$line
\n"; +} + +# format extended diff header line, before patch itself +sub format_extended_diff_header_line { + my $line = shift; + my $diffinfo = shift; + my ($from, $to) = @_; + + # match + if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) { + $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"}, + esc_path($from->{'file'})); + } + if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) { + $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"}, + esc_path($to->{'file'})); + } + # match single + if ($line =~ m/\s(\d{6})$/) { + $line .= ' (' . + file_type_long($1) . + ')'; + } + # match + if ($line =~ oid_nlen_prefix_infix_regex($sha1_len, "index ", ",") | + $line =~ oid_nlen_prefix_infix_regex($sha256_len, "index ", ",")) { + # can match only for combined diff + $line = 'index '; + for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) { + if ($from->{'href'}[$i]) { + $line .= $cgi->a({-href=>$from->{'href'}[$i], + -class=>"hash"}, + substr($diffinfo->{'from_id'}[$i],0,7)); + } else { + $line .= '0' x 7; + } + # separator + $line .= ',' if ($i < $diffinfo->{'nparents'} - 1); + } + $line .= '..'; + if ($to->{'href'}) { + $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"}, + substr($diffinfo->{'to_id'},0,7)); + } else { + $line .= '0' x 7; + } + + } elsif ($line =~ oid_nlen_prefix_infix_regex($sha1_len, "index ", "..") | + $line =~ oid_nlen_prefix_infix_regex($sha256_len, "index ", "..")) { + # can match only for ordinary diff + my ($from_link, $to_link); + if ($from->{'href'}) { + $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"}, + substr($diffinfo->{'from_id'},0,7)); + } else { + $from_link = '0' x 7; + } + if ($to->{'href'}) { + $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"}, + substr($diffinfo->{'to_id'},0,7)); + } else { + $to_link = '0' x 7; + } + my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'}); + $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!; + } + + return $line . "
\n"; +} + +# format from-file/to-file diff header +sub format_diff_from_to_header { + my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_; + my $line; + my $result = ''; + + $line = $from_line; + #assert($line =~ m/^---/) if DEBUG; + # no extra formatting for "^--- /dev/null" + if (! $diffinfo->{'nparents'}) { + # ordinary (single parent) diff + if ($line =~ m!^--- "?a/!) { + if ($from->{'href'}) { + $line = '--- a/' . + $cgi->a({-href=>$from->{'href'}, -class=>"path"}, + esc_path($from->{'file'})); + } else { + $line = '--- a/' . + esc_path($from->{'file'}); + } + } + $result .= qq!
$line
\n!; + + } else { + # combined diff (merge commit) + for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) { + if ($from->{'href'}[$i]) { + $line = '--- ' . + $cgi->a({-href=>href(action=>"blobdiff", + hash_parent=>$diffinfo->{'from_id'}[$i], + hash_parent_base=>$parents[$i], + file_parent=>$from->{'file'}[$i], + hash=>$diffinfo->{'to_id'}, + hash_base=>$hash, + file_name=>$to->{'file'}), + -class=>"path", + -title=>"diff" . ($i+1)}, + $i+1) . + '/' . + $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"}, + esc_path($from->{'file'}[$i])); + } else { + $line = '--- /dev/null'; + } + $result .= qq!
$line
\n!; + } + } + + $line = $to_line; + #assert($line =~ m/^\+\+\+/) if DEBUG; + # no extra formatting for "^+++ /dev/null" + if ($line =~ m!^\+\+\+ "?b/!) { + if ($to->{'href'}) { + $line = '+++ b/' . + $cgi->a({-href=>$to->{'href'}, -class=>"path"}, + esc_path($to->{'file'})); + } else { + $line = '+++ b/' . + esc_path($to->{'file'}); + } + } + $result .= qq!
$line
\n!; + + return $result; +} + +# create note for patch simplified by combined diff +sub format_diff_cc_simplified { + my ($diffinfo, @parents) = @_; + my $result = ''; + + $result .= "
" . + "diff --cc "; + if (!is_deleted($diffinfo)) { + $result .= $cgi->a({-href => href(action=>"blob", + hash_base=>$hash, + hash=>$diffinfo->{'to_id'}, + file_name=>$diffinfo->{'to_file'}), + -class => "path"}, + esc_path($diffinfo->{'to_file'})); + } else { + $result .= esc_path($diffinfo->{'to_file'}); + } + $result .= "
\n" . # class="diff header" + "
" . + "Simple merge" . + "
\n"; # class="diff nodifferences" + + return $result; +} + +sub diff_line_class { + my ($line, $from, $to) = @_; + + # ordinary diff + my $num_sign = 1; + # combined diff + if ($from && $to && ref($from->{'href'}) eq "ARRAY") { + $num_sign = scalar @{$from->{'href'}}; + } + + my @diff_line_classifier = ( + { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"}, + { regexp => qr/^\\/, class => "incomplete" }, + { regexp => qr/^ {$num_sign}/, class => "ctx" }, + # classifier for context must come before classifier add/rem, + # or we would have to use more complicated regexp, for example + # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1; + { regexp => qr/^[+ ]{$num_sign}/, class => "add" }, + { regexp => qr/^[- ]{$num_sign}/, class => "rem" }, + ); + for my $clsfy (@diff_line_classifier) { + return $clsfy->{'class'} + if ($line =~ $clsfy->{'regexp'}); + } + + # fallback + return ""; +} + +# assumes that $from and $to are defined and correctly filled, +# and that $line holds a line of chunk header for unified diff +sub format_unidiff_chunk_header { + my ($line, $from, $to) = @_; + + my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) = + $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/; + + $from_lines = 0 unless defined $from_lines; + $to_lines = 0 unless defined $to_lines; + + if ($from->{'href'}) { + $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start", + -class=>"list"}, $from_text); + } + if ($to->{'href'}) { + $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start", + -class=>"list"}, $to_text); + } + $line = "@@ $from_text $to_text @@" . + "" . esc_html($section, -nbsp=>1) . ""; + return $line; +} + +# assumes that $from and $to are defined and correctly filled, +# and that $line holds a line of chunk header for combined diff +sub format_cc_diff_chunk_header { + my ($line, $from, $to) = @_; + + my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/; + my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines); + + @from_text = split(' ', $ranges); + for (my $i = 0; $i < @from_text; ++$i) { + ($from_start[$i], $from_nlines[$i]) = + (split(',', substr($from_text[$i], 1)), 0); + } + + $to_text = pop @from_text; + $to_start = pop @from_start; + $to_nlines = pop @from_nlines; + + $line = "$prefix "; + for (my $i = 0; $i < @from_text; ++$i) { + if ($from->{'href'}[$i]) { + $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]", + -class=>"list"}, $from_text[$i]); + } else { + $line .= $from_text[$i]; + } + $line .= " "; + } + if ($to->{'href'}) { + $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start", + -class=>"list"}, $to_text); + } else { + $line .= $to_text; + } + $line .= " $prefix" . + "" . esc_html($section, -nbsp=>1) . ""; + return $line; +} + +# process patch (diff) line (not to be used for diff headers), +# returning HTML-formatted (but not wrapped) line. +# If the line is passed as a reference, it is treated as HTML and not +# esc_html()'ed. +sub format_diff_line { + my ($line, $diff_class, $from, $to) = @_; + + if (ref($line)) { + $line = $$line; + } else { + chomp $line; + $line = untabify($line); + + if ($from && $to && $line =~ m/^\@{2} /) { + $line = format_unidiff_chunk_header($line, $from, $to); + } elsif ($from && $to && $line =~ m/^\@{3}/) { + $line = format_cc_diff_chunk_header($line, $from, $to); + } else { + $line = esc_html($line, -nbsp=>1); + } + } + + my $diff_classes = "diff"; + $diff_classes .= " $diff_class" if ($diff_class); + $line = "
$line
\n"; + + return $line; +} + +# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)", +# linked. Pass the hash of the tree/commit to snapshot. +sub format_snapshot_links { + my ($hash) = @_; + my $num_fmts = @snapshot_fmts; + if ($num_fmts > 1) { + # A parenthesized list of links bearing format names. + # e.g. "snapshot (_tar.gz_ _zip_)" + return "snapshot (" . join(' ', map + $cgi->a({ + -href => href( + action=>"snapshot", + hash=>$hash, + snapshot_format=>$_ + ) + }, $known_snapshot_formats{$_}{'display'}) + , @snapshot_fmts) . ")"; + } elsif ($num_fmts == 1) { + # A single "snapshot" link whose tooltip bears the format name. + # i.e. "_snapshot_" + my ($fmt) = @snapshot_fmts; + return + $cgi->a({ + -href => href( + action=>"snapshot", + hash=>$hash, + snapshot_format=>$fmt + ), + -title => "in format: $known_snapshot_formats{$fmt}{'display'}" + }, "snapshot"); + } else { # $num_fmts == 0 + return undef; + } +} + +## ...................................................................... +## functions returning values to be passed, perhaps after some +## transformation, to other functions; e.g. returning arguments to href() + +# returns hash to be passed to href to generate gitweb URL +# in -title key it returns description of link +sub get_feed_info { + my $format = shift || 'Atom'; + my %res = (action => lc($format)); + my $matched_ref = 0; + + # feed links are possible only for project views + return unless (defined $project); + # some views should link to OPML, or to generic project feed, + # or don't have specific feed yet (so they should use generic) + return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x); + + my $branch = undef; + # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix + # (fullname) to differentiate from tag links; this also makes + # possible to detect branch links + for my $ref (get_branch_refs()) { + if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) || + (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) { + $branch = $1; + $matched_ref = $ref; + last; + } + } + # find log type for feed description (title) + my $type = 'log'; + if (defined $file_name) { + $type = "history of $file_name"; + $type .= "/" if ($action eq 'tree'); + $type .= " on '$branch'" if (defined $branch); + } else { + $type = "log of $branch" if (defined $branch); + } + + $res{-title} = $type; + $res{'hash'} = (defined $branch ? "refs/$matched_ref/$branch" : undef); + $res{'file_name'} = $file_name; + + return %res; +} + +## ---------------------------------------------------------------------- +## git utility subroutines, invoking git commands + +# returns path to the core git executable and the --git-dir parameter as list +sub git_cmd { + $number_of_git_cmds++; + return $GIT, '--git-dir='.$git_dir; +} + +# quote the given arguments for passing them to the shell +# quote_command("command", "arg 1", "arg with ' and ! characters") +# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'" +# Try to avoid using this function wherever possible. +sub quote_command { + return join(' ', + map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ); +} + +# get HEAD ref of given project as hash +sub git_get_head_hash { + return git_get_full_hash(shift, 'HEAD'); +} + +sub git_get_full_hash { + return git_get_hash(@_); +} + +sub git_get_short_hash { + return git_get_hash(@_, '--short=7'); +} + +sub git_get_hash { + my ($project, $hash, @options) = @_; + my $o_git_dir = $git_dir; + my $retval = undef; + $git_dir = "$projectroot/$project"; + if (open my $fd, '-|', git_cmd(), 'rev-parse', + '--verify', '-q', @options, $hash) { + $retval = <$fd>; + chomp $retval if defined $retval; + close $fd; + } + if (defined $o_git_dir) { + $git_dir = $o_git_dir; + } + return $retval; +} + +# get type of given object +sub git_get_type { + my $hash = shift; + + open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return; + my $type = <$fd>; + close $fd or return; + chomp $type; + return $type; +} + +# repository configuration +our $config_file = ''; +our %config; + +# store multiple values for single key as anonymous array reference +# single values stored directly in the hash, not as [ ] +sub hash_set_multi { + my ($hash, $key, $value) = @_; + + if (!exists $hash->{$key}) { + $hash->{$key} = $value; + } elsif (!ref $hash->{$key}) { + $hash->{$key} = [ $hash->{$key}, $value ]; + } else { + push @{$hash->{$key}}, $value; + } +} + +# return hash of git project configuration +# optionally limited to some section, e.g. 'gitweb' +sub git_parse_project_config { + my $section_regexp = shift; + my %config; + + local $/ = "\0"; + + open my $fh, "-|", git_cmd(), "config", '-z', '-l', + or return; + + while (my $keyval = <$fh>) { + chomp $keyval; + my ($key, $value) = split(/\n/, $keyval, 2); + + hash_set_multi(\%config, $key, $value) + if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o); + } + close $fh; + + return %config; +} + +# convert config value to boolean: 'true' or 'false' +# no value, number > 0, 'true' and 'yes' values are true +# rest of values are treated as false (never as error) +sub config_to_bool { + my $val = shift; + + return 1 if !defined $val; # section.key + + # strip leading and trailing whitespace + $val =~ s/^\s+//; + $val =~ s/\s+$//; + + return (($val =~ /^\d+$/ && $val) || # section.key = 1 + ($val =~ /^(?:true|yes)$/i)); # section.key = true +} + +# convert config value to simple decimal number +# an optional value suffix of 'k', 'm', or 'g' will cause the value +# to be multiplied by 1024, 1048576, or 1073741824 +sub config_to_int { + my $val = shift; + + # strip leading and trailing whitespace + $val =~ s/^\s+//; + $val =~ s/\s+$//; + + if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) { + $unit = lc($unit); + # unknown unit is treated as 1 + return $num * ($unit eq 'g' ? 1073741824 : + $unit eq 'm' ? 1048576 : + $unit eq 'k' ? 1024 : 1); + } + return $val; +} + +# convert config value to array reference, if needed +sub config_to_multi { + my $val = shift; + + return ref($val) ? $val : (defined($val) ? [ $val ] : []); +} + +sub git_get_project_config { + my ($key, $type) = @_; + + return unless defined $git_dir; + + # key sanity check + return unless ($key); + # only subsection, if exists, is case sensitive, + # and not lowercased by 'git config -z -l' + if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) { + $lo =~ s/_//g; + $key = join(".", lc($hi), $mi, lc($lo)); + return if ($lo =~ /\W/ || $hi =~ /\W/); + } else { + $key = lc($key); + $key =~ s/_//g; + return if ($key =~ /\W/); + } + $key =~ s/^gitweb\.//; + + # type sanity check + if (defined $type) { + $type =~ s/^--//; + $type = undef + unless ($type eq 'bool' || $type eq 'int'); + } + + # get config + if (!defined $config_file || + $config_file ne "$git_dir/config") { + %config = git_parse_project_config('gitweb'); + $config_file = "$git_dir/config"; + } + + # check if config variable (key) exists + return unless exists $config{"gitweb.$key"}; + + # ensure given type + if (!defined $type) { + return $config{"gitweb.$key"}; + } elsif ($type eq 'bool') { + # backward compatibility: 'git config --bool' returns true/false + return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false'; + } elsif ($type eq 'int') { + return config_to_int($config{"gitweb.$key"}); + } + return $config{"gitweb.$key"}; +} + +# get hash of given path at given ref +sub git_get_hash_by_path { + my $base = shift; + my $path = shift || return undef; + my $type = shift; + + $path =~ s,/+$,,; + + open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path + or die_error(500, "Open git-ls-tree failed"); + my $line = <$fd>; + close $fd or return undef; + + if (!defined $line) { + # there is no tree or hash given by $path at $base + return undef; + } + + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' + $line =~ m/^([0-9]+) (.+) ($oid_regex)\t/; + if (defined $type && $type ne $2) { + # type doesn't match + return undef; + } + return $3; +} + +# get path of entry with given hash at given tree-ish (ref) +# used to get 'from' filename for combined diff (merge commit) for renames +sub git_get_path_by_hash { + my $base = shift || return; + my $hash = shift || return; + + local $/ = "\0"; + + open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base + or return undef; + while (my $line = <$fd>) { + chomp $line; + + #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb' + #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README' + if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) { + close $fd; + return $1; + } + } + close $fd; + return undef; +} + +## ...................................................................... +## git utility functions, directly accessing git repository + +# get the value of config variable either from file named as the variable +# itself in the repository ($GIT_DIR/$name file), or from gitweb.$name +# configuration variable in the repository config file. +sub git_get_file_or_project_config { + my ($path, $name) = @_; + + $git_dir = "$projectroot/$path"; + open my $fd, '<', "$git_dir/$name" + or return git_get_project_config($name); + my $conf = <$fd>; + close $fd; + if (defined $conf) { + chomp $conf; + } + return $conf; +} + +sub git_get_project_description { + my $path = shift; + return git_get_file_or_project_config($path, 'description'); +} + +sub git_get_project_category { + my $path = shift; + return git_get_file_or_project_config($path, 'category'); +} + + +# supported formats: +# * $GIT_DIR/ctags/ file (in 'ctags' subdirectory) +# - if its contents is a number, use it as tag weight, +# - otherwise add a tag with weight 1 +# * $GIT_DIR/ctags file, each line is a tag (with weight 1) +# the same value multiple times increases tag weight +# * `gitweb.ctag' multi-valued repo config variable +sub git_get_project_ctags { + my $project = shift; + my $ctags = {}; + + $git_dir = "$projectroot/$project"; + if (opendir my $dh, "$git_dir/ctags") { + my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh); + foreach my $tagfile (@files) { + open my $ct, '<', $tagfile + or next; + my $val = <$ct>; + chomp $val if $val; + close $ct; + + (my $ctag = $tagfile) =~ s#.*/##; + if ($val =~ /^\d+$/) { + $ctags->{$ctag} = $val; + } else { + $ctags->{$ctag} = 1; + } + } + closedir $dh; + + } elsif (open my $fh, '<', "$git_dir/ctags") { + while (my $line = <$fh>) { + chomp $line; + $ctags->{$line}++ if $line; + } + close $fh; + + } else { + my $taglist = config_to_multi(git_get_project_config('ctag')); + foreach my $tag (@$taglist) { + $ctags->{$tag}++; + } + } + + return $ctags; +} + +# return hash, where keys are content tags ('ctags'), +# and values are sum of weights of given tag in every project +sub git_gather_all_ctags { + my $projects = shift; + my $ctags = {}; + + foreach my $p (@$projects) { + foreach my $ct (keys %{$p->{'ctags'}}) { + $ctags->{$ct} += $p->{'ctags'}->{$ct}; + } + } + + return $ctags; +} + +sub git_populate_project_tagcloud { + my $ctags = shift; + + # First, merge different-cased tags; tags vote on casing + my %ctags_lc; + foreach (keys %$ctags) { + $ctags_lc{lc $_}->{count} += $ctags->{$_}; + if (not $ctags_lc{lc $_}->{topcount} + or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) { + $ctags_lc{lc $_}->{topcount} = $ctags->{$_}; + $ctags_lc{lc $_}->{topname} = $_; + } + } + + my $cloud; + my $matched = $input_params{'ctag'}; + if (eval { require HTML::TagCloud; 1; }) { + $cloud = HTML::TagCloud->new; + foreach my $ctag (sort keys %ctags_lc) { + # Pad the title with spaces so that the cloud looks + # less crammed. + my $title = esc_html($ctags_lc{$ctag}->{topname}); + $title =~ s/ / /g; + $title =~ s/^/ /g; + $title =~ s/$/ /g; + if (defined $matched && $matched eq $ctag) { + $title = qq($title); + } + $cloud->add($title, href(project=>undef, ctag=>$ctag), + $ctags_lc{$ctag}->{count}); + } + } else { + $cloud = {}; + foreach my $ctag (keys %ctags_lc) { + my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1); + if (defined $matched && $matched eq $ctag) { + $title = qq($title); + } + $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count}; + $cloud->{$ctag}{ctag} = + $cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title); + } + } + return $cloud; +} + +sub git_show_project_tagcloud { + my ($cloud, $count) = @_; + if (ref $cloud eq 'HTML::TagCloud') { + return $cloud->html_and_css($count); + } else { + my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud; + return + '
' . + join (', ', map { + $cloud->{$_}->{'ctag'} + } splice(@tags, 0, $count)) . + '
'; + } +} + +sub git_get_project_url_list { + my $path = shift; + + $git_dir = "$projectroot/$path"; + open my $fd, '<', "$git_dir/cloneurl" + or return wantarray ? + @{ config_to_multi(git_get_project_config('url')) } : + config_to_multi(git_get_project_config('url')); + my @git_project_url_list = map { chomp; $_ } <$fd>; + close $fd; + + return wantarray ? @git_project_url_list : \@git_project_url_list; +} + +sub git_get_projects_list { + my $filter = shift || ''; + my $paranoid = shift; + my @list; + + if (-d $projects_list) { + # search in directory + my $dir = $projects_list; + # remove the trailing "/" + $dir =~ s!/+$!!; + my $pfxlen = length("$dir"); + my $pfxdepth = ($dir =~ tr!/!!); + # when filtering, search only given subdirectory + if ($filter && !$paranoid) { + $dir .= "/$filter"; + $dir =~ s!/+$!!; + } + + File::Find::find({ + follow_fast => 1, # follow symbolic links + follow_skip => 2, # ignore duplicates + dangling_symlinks => 0, # ignore dangling symlinks, silently + wanted => sub { + # global variables + our $project_maxdepth; + our $projectroot; + # skip project-list toplevel, if we get it. + return if (m!^[/.]$!); + # only directories can be git repositories + return unless (-d $_); + # need search permission + return unless (-x $_); + # don't traverse too deep (Find is super slow on os x) + # $project_maxdepth excludes depth of $projectroot + if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) { + $File::Find::prune = 1; + return; + } + + my $path = substr($File::Find::name, $pfxlen + 1); + # paranoidly only filter here + if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) { + next; + } + # we check related file in $projectroot + if (check_export_ok("$projectroot/$path")) { + push @list, { path => $path }; + $File::Find::prune = 1; + } + }, + }, "$dir"); + + } elsif (-f $projects_list) { + # read from file(url-encoded): + # 'git%2Fgit.git Linus+Torvalds' + # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin' + # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman' + open my $fd, '<', $projects_list or return; + PROJECT: + while (my $line = <$fd>) { + chomp $line; + my ($path, $owner) = split ' ', $line; + $path = unescape($path); + $owner = unescape($owner); + if (!defined $path) { + next; + } + # if $filter is rpovided, check if $path begins with $filter + if ($filter && $path !~ m!^\Q$filter\E/!) { + next; + } + if (check_export_ok("$projectroot/$path")) { + my $pr = { + path => $path + }; + if ($owner) { + $pr->{'owner'} = to_utf8($owner); + } + push @list, $pr; + } + } + close $fd; + } + return @list; +} + +# written with help of Tree::Trie module (Perl Artistic License, GPL compatible) +# as side effects it sets 'forks' field to list of forks for forked projects +sub filter_forks_from_projects_list { + my $projects = shift; + + my %trie; # prefix tree of directories (path components) + # generate trie out of those directories that might contain forks + foreach my $pr (@$projects) { + my $path = $pr->{'path'}; + $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory + next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git' + next unless ($path); # skip '.git' repository: tests, git-instaweb + next unless (-d "$projectroot/$path"); # containing directory exists + $pr->{'forks'} = []; # there can be 0 or more forks of project + + # add to trie + my @dirs = split('/', $path); + # walk the trie, until either runs out of components or out of trie + my $ref = \%trie; + while (scalar @dirs && + exists($ref->{$dirs[0]})) { + $ref = $ref->{shift @dirs}; + } + # create rest of trie structure from rest of components + foreach my $dir (@dirs) { + $ref = $ref->{$dir} = {}; + } + # create end marker, store $pr as a data + $ref->{''} = $pr if (!exists $ref->{''}); + } + + # filter out forks, by finding shortest prefix match for paths + my @filtered; + PROJECT: + foreach my $pr (@$projects) { + # trie lookup + my $ref = \%trie; + DIR: + foreach my $dir (split('/', $pr->{'path'})) { + if (exists $ref->{''}) { + # found [shortest] prefix, is a fork - skip it + push @{$ref->{''}{'forks'}}, $pr; + next PROJECT; + } + if (!exists $ref->{$dir}) { + # not in trie, cannot have prefix, not a fork + push @filtered, $pr; + next PROJECT; + } + # If the dir is there, we just walk one step down the trie. + $ref = $ref->{$dir}; + } + # we ran out of trie + # (shouldn't happen: it's either no match, or end marker) + push @filtered, $pr; + } + + return @filtered; +} + +# note: fill_project_list_info must be run first, +# for 'descr_long' and 'ctags' to be filled +sub search_projects_list { + my ($projlist, %opts) = @_; + my $tagfilter = $opts{'tagfilter'}; + my $search_re = $opts{'search_regexp'}; + + return @$projlist + unless ($tagfilter || $search_re); + + # searching projects require filling to be run before it; + fill_project_list_info($projlist, + $tagfilter ? 'ctags' : (), + $search_re ? ('path', 'descr') : ()); + my @projects; + PROJECT: + foreach my $pr (@$projlist) { + + if ($tagfilter) { + next unless ref($pr->{'ctags'}) eq 'HASH'; + next unless + grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}}; + } + + if ($search_re) { + next unless + $pr->{'path'} =~ /$search_re/ || + $pr->{'descr_long'} =~ /$search_re/; + } + + push @projects, $pr; + } + + return @projects; +} + +our $gitweb_project_owner = undef; +sub git_get_project_list_from_file { + + return if (defined $gitweb_project_owner); + + $gitweb_project_owner = {}; + # read from file (url-encoded): + # 'git%2Fgit.git Linus+Torvalds' + # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin' + # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman' + if (-f $projects_list) { + open(my $fd, '<', $projects_list); + while (my $line = <$fd>) { + chomp $line; + my ($pr, $ow) = split ' ', $line; + $pr = unescape($pr); + $ow = unescape($ow); + $gitweb_project_owner->{$pr} = to_utf8($ow); + } + close $fd; + } +} + +sub git_get_project_owner { + my $project = shift; + my $owner; + + return undef unless $project; + $git_dir = "$projectroot/$project"; + + if (!defined $gitweb_project_owner) { + git_get_project_list_from_file(); + } + + if (exists $gitweb_project_owner->{$project}) { + $owner = $gitweb_project_owner->{$project}; + } + if (!defined $owner){ + $owner = git_get_project_config('owner'); + } + if (!defined $owner) { + $owner = get_file_owner("$git_dir"); + } + + return $owner; +} + +sub git_get_last_activity { + my ($path) = @_; + my $fd; + + $git_dir = "$projectroot/$path"; + open($fd, "-|", git_cmd(), 'for-each-ref', + '--format=%(committer)', + '--sort=-committerdate', + '--count=1', + map { "refs/$_" } get_branch_refs ()) or return; + my $most_recent = <$fd>; + close $fd or return; + if (defined $most_recent && + $most_recent =~ / (\d+) [-+][01]\d\d\d$/) { + my $timestamp = $1; + my $age = time - $timestamp; + return ($age, age_string($age)); + } + return (undef, undef); +} + +sub git_get_latest_tag { + my ($path) = @_; + my $fd; + + $git_dir = "$projectroot/$path"; + open($fd, "-|", git_cmd(), 'describe', '--tags', '--abbrev=0', 'HEAD', '2>/dev/null') or return ""; + my $tag = <$fd>; + close $fd; + if (defined $tag) { + chomp $tag; + return $tag; + } + return ""; +} + +# Implementation note: when a single remote is wanted, we cannot use 'git +# remote show -n' because that command always work (assuming it's a remote URL +# if it's not defined), and we cannot use 'git remote show' because that would +# try to make a network roundtrip. So the only way to find if that particular +# remote is defined is to walk the list provided by 'git remote -v' and stop if +# and when we find what we want. +sub git_get_remotes_list { + my $wanted = shift; + my %remotes = (); + + open my $fd, '-|' , git_cmd(), 'remote', '-v'; + return unless $fd; + while (my $remote = <$fd>) { + chomp $remote; + $remote =~ s!\t(.*?)\s+\((\w+)\)$!!; + next if $wanted and not $remote eq $wanted; + my ($url, $key) = ($1, $2); + + $remotes{$remote} ||= { 'heads' => () }; + $remotes{$remote}{$key} = $url; + } + close $fd or return; + return wantarray ? %remotes : \%remotes; +} + +# Takes a hash of remotes as first parameter and fills it by adding the +# available remote heads for each of the indicated remotes. +sub fill_remote_heads { + my $remotes = shift; + my @heads = map { "remotes/$_" } keys %$remotes; + my @remoteheads = git_get_heads_list(undef, @heads); + foreach my $remote (keys %$remotes) { + $remotes->{$remote}{'heads'} = [ grep { + $_->{'name'} =~ s!^$remote/!! + } @remoteheads ]; + } +} + +sub git_get_references { + my $type = shift || ""; + my %refs; + # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11 + # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{} + open my $fd, "-|", git_cmd(), "show-ref", "--dereference", + ($type ? ("--", "refs/$type") : ()) # use -- if $type + or return; + + while (my $line = <$fd>) { + chomp $line; + if ($line =~ m!^($oid_regex)\srefs/($type.*)$!) { + if (defined $refs{$1}) { + push @{$refs{$1}}, $2; + } else { + $refs{$1} = [ $2 ]; + } + } + } + close $fd or return; + return \%refs; +} + +sub git_get_rev_name_tags { + my $hash = shift || return undef; + + open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash + or return; + my $name_rev = <$fd>; + close $fd; + + if ($name_rev =~ m|^$hash tags/(.*)$|) { + return $1; + } else { + # catches also '$hash undefined' output + return undef; + } +} + +## ---------------------------------------------------------------------- +## parse to hash functions + +sub parse_date { + my $epoch = shift; + my $tz = shift || "-0000"; + + my %date; + my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"); + my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"); + my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch); + $date{'hour'} = $hour; + $date{'minute'} = $min; + $date{'mday'} = $mday; + $date{'day'} = $days[$wday]; + $date{'month'} = $months[$mon]; + $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000", + $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec; + $date{'mday-time'} = sprintf "%d %s %02d:%02d", + $mday, $months[$mon], $hour ,$min; + $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ", + 1900+$year, 1+$mon, $mday, $hour ,$min, $sec; + + my ($tz_sign, $tz_hour, $tz_min) = + ($tz =~ m/^([-+])(\d\d)(\d\d)$/); + $tz_sign = ($tz_sign eq '-' ? -1 : +1); + my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60); + ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local); + $date{'hour_local'} = $hour; + $date{'minute_local'} = $min; + $date{'tz_local'} = $tz; + $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s", + 1900+$year, $mon+1, $mday, + $hour, $min, $sec, $tz); + return %date; +} + +sub hide_mailaddrs_if_private { + my $line = shift; + return $line unless gitweb_check_feature('email-privacy'); + $line =~ s/<[^@>]+@[^>]+>//g; + return $line; +} + +sub parse_tag { + my $tag_id = shift; + my %tag; + my @comment; + + open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return; + $tag{'id'} = $tag_id; + while (my $line = <$fd>) { + chomp $line; + if ($line =~ m/^object ($oid_regex)$/) { + $tag{'object'} = $1; + } elsif ($line =~ m/^type (.+)$/) { + $tag{'type'} = $1; + } elsif ($line =~ m/^tag (.+)$/) { + $tag{'name'} = $1; + } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) { + $tag{'author'} = hide_mailaddrs_if_private($1); + $tag{'author_epoch'} = $2; + $tag{'author_tz'} = $3; + if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) { + $tag{'author_name'} = $1; + $tag{'author_email'} = $2; + } else { + $tag{'author_name'} = $tag{'author'}; + } + } elsif ($line =~ m/--BEGIN/) { + push @comment, $line; + last; + } elsif ($line eq "") { + last; + } + } + push @comment, <$fd>; + $tag{'comment'} = \@comment; + close $fd or return; + if (!defined $tag{'name'}) { + return + }; + return %tag +} + +sub parse_commit_text { + my ($commit_text, $withparents) = @_; + my @commit_lines = split '\n', $commit_text; + my %co; + + pop @commit_lines; # Remove '\0' + + if (! @commit_lines) { + return; + } + + my $header = shift @commit_lines; + if ($header !~ m/^$oid_regex/) { + return; + } + ($co{'id'}, my @parents) = split ' ', $header; + while (my $line = shift @commit_lines) { + last if $line eq "\n"; + if ($line =~ m/^tree ($oid_regex)$/) { + $co{'tree'} = $1; + } elsif ((!defined $withparents) && ($line =~ m/^parent ($oid_regex)$/)) { + push @parents, $1; + } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) { + $co{'author'} = hide_mailaddrs_if_private(to_utf8($1)); + $co{'author_epoch'} = $2; + $co{'author_tz'} = $3; + if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) { + $co{'author_name'} = $1; + $co{'author_email'} = $2; + } else { + $co{'author_name'} = $co{'author'}; + } + } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) { + $co{'committer'} = hide_mailaddrs_if_private(to_utf8($1)); + $co{'committer_epoch'} = $2; + $co{'committer_tz'} = $3; + if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) { + $co{'committer_name'} = $1; + $co{'committer_email'} = $2; + } else { + $co{'committer_name'} = $co{'committer'}; + } + } + } + if (!defined $co{'tree'}) { + return; + }; + $co{'parents'} = \@parents; + $co{'parent'} = $parents[0]; + + foreach my $title (@commit_lines) { + $title =~ s/^ //; + if ($title ne "") { + $co{'title'} = chop_str($title, 80, 5); + $co{'title_short'} = chop_str($title, 50, 5); + last; + } + } + if (! defined $co{'title'} || $co{'title'} eq "") { + $co{'title'} = $co{'title_short'} = '(no commit message)'; + } + # remove added spaces, redact e-mail addresses if applicable. + foreach my $line (@commit_lines) { + $line =~ s/^ //; + $line = hide_mailaddrs_if_private($line); + } + $co{'comment'} = \@commit_lines; + + my $age = time - $co{'committer_epoch'}; + $co{'age'} = $age; + $co{'age_string'} = age_string($age); + my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'}); + if ($age > 60*60*24*7*2) { + $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday; + $co{'age_string_age'} = $co{'age_string'}; + } else { + $co{'age_string_date'} = $co{'age_string'}; + $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday; + } + return %co; +} + +sub parse_commit { + my ($commit_id) = @_; + my %co; + + local $/ = "\0"; + + open my $fd, "-|", git_cmd(), "rev-list", + "--parents", + "--header", + "--max-count=1", + $commit_id, + "--", + or die_error(500, "Open git-rev-list failed"); + %co = parse_commit_text(<$fd>, 1); + close $fd; + + return %co; +} + +sub parse_commits { + my ($commit_id, $maxcount, $skip, $filename, @args) = @_; + my @cos; + + $maxcount ||= 1; + $skip ||= 0; + + local $/ = "\0"; + + open my $fd, "-|", git_cmd(), "rev-list", + "--header", + @args, + ("--max-count=" . $maxcount), + ("--skip=" . $skip), + @extra_options, + $commit_id, + "--", + ($filename ? ($filename) : ()) + or die_error(500, "Open git-rev-list failed"); + while (my $line = <$fd>) { + my %co = parse_commit_text($line); + push @cos, \%co; + } + close $fd; + + return wantarray ? @cos : \@cos; +} + +# parse line of git-diff-tree "raw" output +sub parse_difftree_raw_line { + my $line = shift; + my %res; + + # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c' + # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c' + if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ($oid_regex) ($oid_regex) (.)([0-9]{0,3})\t(.*)$/) { + $res{'from_mode'} = $1; + $res{'to_mode'} = $2; + $res{'from_id'} = $3; + $res{'to_id'} = $4; + $res{'status'} = $5; + $res{'similarity'} = $6; + if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied + ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7); + } else { + $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7); + } + } + # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh' + # combined diff (for merge commit) + elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:$oid_regex )+)([a-zA-Z]+)\t(.*)$//) { + $res{'nparents'} = length($1); + $res{'from_mode'} = [ split(' ', $2) ]; + $res{'to_mode'} = pop @{$res{'from_mode'}}; + $res{'from_id'} = [ split(' ', $3) ]; + $res{'to_id'} = pop @{$res{'from_id'}}; + $res{'status'} = [ split('', $4) ]; + $res{'to_file'} = unquote($5); + } + # 'c512b523472485aef4fff9e57b229d9d243c967f' + elsif ($line =~ m/^($oid_regex)$/) { + $res{'commit'} = $1; + } + + return wantarray ? %res : \%res; +} + +# wrapper: return parsed line of git-diff-tree "raw" output +# (the argument might be raw line, or parsed info) +sub parsed_difftree_line { + my $line_or_ref = shift; + + if (ref($line_or_ref) eq "HASH") { + # pre-parsed (or generated by hand) + return $line_or_ref; + } else { + return parse_difftree_raw_line($line_or_ref); + } +} + +# parse line of git-ls-tree output +sub parse_ls_tree_line { + my $line = shift; + my %opts = @_; + my %res; + + if ($opts{'-l'}) { + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c' + $line =~ m/^([0-9]+) (.+) ($oid_regex) +(-|[0-9]+)\t(.+)$/s; + + $res{'mode'} = $1; + $res{'type'} = $2; + $res{'hash'} = $3; + $res{'size'} = $4; + if ($opts{'-z'}) { + $res{'name'} = $5; + } else { + $res{'name'} = unquote($5); + } + } else { + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' + $line =~ m/^([0-9]+) (.+) ($oid_regex)\t(.+)$/s; + + $res{'mode'} = $1; + $res{'type'} = $2; + $res{'hash'} = $3; + if ($opts{'-z'}) { + $res{'name'} = $4; + } else { + $res{'name'} = unquote($4); + } + } + + return wantarray ? %res : \%res; +} + +# generates _two_ hashes, references to which are passed as 2 and 3 argument +sub parse_from_to_diffinfo { + my ($diffinfo, $from, $to, @parents) = @_; + + if ($diffinfo->{'nparents'}) { + # combined diff + $from->{'file'} = []; + $from->{'href'} = []; + fill_from_file_info($diffinfo, @parents) + unless exists $diffinfo->{'from_file'}; + for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) { + $from->{'file'}[$i] = + defined $diffinfo->{'from_file'}[$i] ? + $diffinfo->{'from_file'}[$i] : + $diffinfo->{'to_file'}; + if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file + $from->{'href'}[$i] = href(action=>"blob", + hash_base=>$parents[$i], + hash=>$diffinfo->{'from_id'}[$i], + file_name=>$from->{'file'}[$i]); + } else { + $from->{'href'}[$i] = undef; + } + } + } else { + # ordinary (not combined) diff + $from->{'file'} = $diffinfo->{'from_file'}; + if ($diffinfo->{'status'} ne "A") { # not new (added) file + $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent, + hash=>$diffinfo->{'from_id'}, + file_name=>$from->{'file'}); + } else { + delete $from->{'href'}; + } + } + + $to->{'file'} = $diffinfo->{'to_file'}; + if (!is_deleted($diffinfo)) { # file exists in result + $to->{'href'} = href(action=>"blob", hash_base=>$hash, + hash=>$diffinfo->{'to_id'}, + file_name=>$to->{'file'}); + } else { + delete $to->{'href'}; + } +} + +## ...................................................................... +## parse to array of hashes functions + +sub git_get_heads_list { + my ($limit, @classes) = @_; + @classes = get_branch_refs() unless @classes; + my @patterns = map { "refs/$_" } @classes; + my @headslist; + + open my $fd, '-|', git_cmd(), 'for-each-ref', + ($limit ? '--count='.($limit+1) : ()), + '--sort=-HEAD', '--sort=-committerdate', + '--format=%(objectname) %(refname) %(subject)%00%(committer)', + @patterns + or return; + while (my $line = <$fd>) { + my %ref_item; + + chomp $line; + my ($refinfo, $committerinfo) = split(/\0/, $line); + my ($hash, $name, $title) = split(' ', $refinfo, 3); + my ($committer, $epoch, $tz) = + ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/); + $ref_item{'fullname'} = $name; + my $strip_refs = join '|', map { quotemeta } get_branch_refs(); + $name =~ s!^refs/($strip_refs|remotes)/!!; + $ref_item{'name'} = $name; + # for refs neither in 'heads' nor 'remotes' we want to + # show their ref dir + my $ref_dir = (defined $1) ? $1 : ''; + if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') { + $ref_item{'name'} .= ' (' . $ref_dir . ')'; + } + + $ref_item{'id'} = $hash; + $ref_item{'title'} = $title || '(no commit message)'; + $ref_item{'epoch'} = $epoch; + if ($epoch) { + $ref_item{'age'} = age_string(time - $ref_item{'epoch'}); + } else { + $ref_item{'age'} = "unknown"; + } + + push @headslist, \%ref_item; + } + close $fd; + + return wantarray ? @headslist : \@headslist; +} + +sub git_get_tags_list { + my $limit = shift; + my @tagslist; + + open my $fd, '-|', git_cmd(), 'for-each-ref', + ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate', + '--format=%(objectname) %(objecttype) %(refname) '. + '%(*objectname) %(*objecttype) %(subject)%00%(creator)', + 'refs/tags' + or return; + while (my $line = <$fd>) { + my %ref_item; + + chomp $line; + my ($refinfo, $creatorinfo) = split(/\0/, $line); + my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6); + my ($creator, $epoch, $tz) = + ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/); + $ref_item{'fullname'} = $name; + $name =~ s!^refs/tags/!!; + + $ref_item{'type'} = $type; + $ref_item{'id'} = $id; + $ref_item{'name'} = $name; + if ($type eq "tag") { + $ref_item{'subject'} = $title; + $ref_item{'reftype'} = $reftype; + $ref_item{'refid'} = $refid; + } else { + $ref_item{'reftype'} = $type; + $ref_item{'refid'} = $id; + } + + if ($type eq "tag" || $type eq "commit") { + $ref_item{'epoch'} = $epoch; + if ($epoch) { + $ref_item{'age'} = age_string(time - $ref_item{'epoch'}); + } else { + $ref_item{'age'} = "unknown"; + } + } + + push @tagslist, \%ref_item; + } + close $fd; + + return wantarray ? @tagslist : \@tagslist; +} + +## ---------------------------------------------------------------------- +## filesystem-related functions + +sub get_file_owner { + my $path = shift; + + my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path); + my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid); + if (!defined $gcos) { + return undef; + } + my $owner = $gcos; + $owner =~ s/[,;].*$//; + return to_utf8($owner); +} + +# assume that file exists +sub insert_file { + my $filename = shift; + + open my $fd, '<', $filename; + print map { to_utf8($_) } <$fd>; + close $fd; +} + +## ...................................................................... +## mimetype related functions + +sub mimetype_guess_file { + my $filename = shift; + my $mimemap = shift; + -r $mimemap or return undef; + + my %mimemap; + open(my $mh, '<', $mimemap) or return undef; + while (<$mh>) { + next if m/^#/; # skip comments + my ($mimetype, @exts) = split(/\s+/); + foreach my $ext (@exts) { + $mimemap{$ext} = $mimetype; + } + } + close($mh); + + $filename =~ /\.([^.]*)$/; + return $mimemap{$1}; +} + +sub mimetype_guess { + my $filename = shift; + my $mime; + $filename =~ /\./ or return undef; + + if ($mimetypes_file) { + my $file = $mimetypes_file; + if ($file !~ m!^/!) { # if it is relative path + # it is relative to project + $file = "$projectroot/$project/$file"; + } + $mime = mimetype_guess_file($filename, $file); + } + $mime ||= mimetype_guess_file($filename, '/etc/mime.types'); + return $mime; +} + +sub blob_mimetype { + my $fd = shift; + my $filename = shift; + + if ($filename) { + my $mime = mimetype_guess($filename); + $mime and return $mime; + } + + # just in case + return $default_blob_plain_mimetype unless $fd; + + if (-T $fd) { + return 'text/plain'; + } elsif (! $filename) { + return 'application/octet-stream'; + } elsif ($filename =~ m/\.png$/i) { + return 'image/png'; + } elsif ($filename =~ m/\.gif$/i) { + return 'image/gif'; + } elsif ($filename =~ m/\.jpe?g$/i) { + return 'image/jpeg'; + } else { + return 'application/octet-stream'; + } +} + +sub blob_contenttype { + my ($fd, $file_name, $type) = @_; + + $type ||= blob_mimetype($fd, $file_name); + if ($type eq 'text/plain' && defined $default_text_plain_charset) { + $type .= "; charset=$default_text_plain_charset"; + } + + return $type; +} + +# guess file syntax for syntax highlighting; return undef if no highlighting +# the name of syntax can (in the future) depend on syntax highlighter used +sub guess_file_syntax { + my ($highlight, $file_name) = @_; + return undef unless ($highlight && defined $file_name); + my $basename = basename($file_name, '.in'); + return $highlight_basename{$basename} + if exists $highlight_basename{$basename}; + + $basename =~ /\.([^.]*)$/; + my $ext = $1 or return undef; + return $highlight_ext{$ext} + if exists $highlight_ext{$ext}; + + return undef; +} + +# run highlighter and return FD of its output, +# or return original FD if no highlighting +sub run_highlighter { + my ($fd, $highlight, $syntax) = @_; + return $fd unless ($highlight); + + close $fd; + my $syntax_arg = (defined $syntax) ? "--syntax $syntax" : "--force"; + open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ". + quote_command($^X, '-CO', '-MEncode=decode,FB_DEFAULT', '-pse', + '$_ = decode($fe, $_, FB_DEFAULT) if !utf8::decode($_);', + '--', "-fe=$fallback_encoding")." | ". + quote_command($highlight_bin). + " --replace-tabs=8 --fragment $syntax_arg |" + or die_error(500, "Couldn't open file or run syntax highlighter"); + return $fd; +} + +## ====================================================================== +## functions printing HTML: header, footer, error page + +sub get_page_title { + my $title = to_utf8($site_name); + + unless (defined $project) { + if (defined $project_filter) { + $title .= " - projects in '" . esc_path($project_filter) . "'"; + } + return $title; + } + $title .= " - " . to_utf8($project); + + return $title unless (defined $action); + $title .= "/$action"; # $action is US-ASCII (7bit ASCII) + + return $title unless (defined $file_name); + $title .= " - " . esc_path($file_name); + if ($action eq "tree" && $file_name !~ m|/$|) { + $title .= "/"; + } + + return $title; +} + +sub get_content_type_html { + # require explicit support from the UA if we are to send the page as + # 'application/xhtml+xml', otherwise send it as plain old 'text/html'. + # we have to do this because MSIE sometimes globs '*/*', pretending to + # support xhtml+xml but choking when it gets what it asked for. + if (defined $cgi->http('HTTP_ACCEPT') && + $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ && + $cgi->Accept('application/xhtml+xml') != 0) { + return 'application/xhtml+xml'; + } else { + return 'text/html'; + } +} + +sub print_feed_meta { + if (defined $project) { + my %href_params = get_feed_info(); + if (!exists $href_params{'-title'}) { + $href_params{'-title'} = 'log'; + } + + foreach my $format (qw(RSS Atom)) { + my $type = lc($format); + my %link_attr = ( + '-rel' => 'alternate', + '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"), + '-type' => "application/$type+xml" + ); + + $href_params{'extra_options'} = undef; + $href_params{'action'} = $type; + $link_attr{'-href'} = esc_attr(href(%href_params)); + print "\n"; + + $href_params{'extra_options'} = '--no-merges'; + $link_attr{'-href'} = esc_attr(href(%href_params)); + $link_attr{'-title'} .= ' (no merges)'; + print "\n"; + } + + } else { + printf(''."\n", + esc_attr($site_name), + esc_attr(href(project=>undef, action=>"project_index"))); + printf(''."\n", + esc_attr($site_name), + esc_attr(href(project=>undef, action=>"opml"))); + } +} + +sub print_header_links { + my $status = shift; + + # print out each stylesheet that exist, providing backwards capability + # for those people who defined $stylesheet in a config file + if (defined $stylesheet) { + print ''."\n"; + } else { + foreach my $stylesheet (@stylesheets) { + next unless $stylesheet; + print ''."\n"; + } + } + print_feed_meta() + if ($status eq '200 OK'); + if (defined $favicon) { + print qq(\n); + } +} + +sub print_nav_breadcrumbs_path { + my $dirprefix = undef; + while (my $part = shift) { + $dirprefix .= "/" if defined $dirprefix; + $dirprefix .= $part; + print $cgi->a({-href => href(project => undef, + project_filter => $dirprefix, + action => "project_list")}, + esc_html($part)) . " / "; + } +} + +sub print_nav_breadcrumbs { + my %opts = @_; + + for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) { + print $cgi->a({-href => esc_url($crumb->[1])}, $crumb->[0]) . " / "; + } + if (defined $project) { + my @dirname = split '/', $project; + my $projectbasename = pop @dirname; + print_nav_breadcrumbs_path(@dirname); + print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename)); + if (defined $action) { + my $action_print = $action ; + if (defined $opts{-action_extra}) { + $action_print = $cgi->a({-href => href(action=>$action)}, + $action); + } + print " / $action_print"; + } + if (defined $opts{-action_extra}) { + print " / $opts{-action_extra}"; + } + print "\n"; + } elsif (defined $project_filter) { + print_nav_breadcrumbs_path(split '/', $project_filter); + } +} + +sub print_search_form { + if (!defined $searchtext) { + $searchtext = ""; + } + my $search_hash; + if (defined $hash_base) { + $search_hash = $hash_base; + } elsif (defined $hash) { + $search_hash = $hash; + } else { + $search_hash = "HEAD"; + } + my $action = $my_uri; + my $use_pathinfo = gitweb_check_feature('pathinfo'); + if ($use_pathinfo) { + $action .= "/".esc_url($project); + } + print $cgi->start_form(-method => "get", -action => $action) . + "
\n" . + (!$use_pathinfo && + $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") . + $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" . + $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" . + $cgi->popup_menu(-name => 'st', -default => 'commit', + -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) . + " " . $cgi->a({-href => href(action=>"search_help"), + -title => "search help" }, "?") . " search:\n", + $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" . + "" . + $cgi->checkbox(-name => 'sr', -value => 1, -label => 're', + -checked => $search_use_regexp) . + "" . + "
" . + $cgi->end_form() . "\n"; +} + +sub git_header_html { + my $status = shift || "200 OK"; + my $expires = shift; + my %opts = @_; + + my $title = get_page_title(); + print $cgi->header(-type=>get_content_type_html(), -charset => 'utf-8', + -status=> $status, -expires => $expires) + unless ($opts{'-no_http_header'}); + my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : ''; + print < + + +]> + + + + + + + +$title +EOF + # the stylesheet, favicon etc urls won't work correctly with path_info + # unless we set the appropriate base URL + if ($ENV{'PATH_INFO'}) { + print "\n"; + } + print_header_links($status); + + if (defined $site_html_head_string) { + print to_utf8($site_html_head_string); + } + + print "\n" . + "\n"; + + if (defined $site_header && -f $site_header) { + insert_file($site_header); + } + + print "
\n"; + if (defined $logo) { + print $cgi->a({-href => esc_url($logo_url), + -title => $logo_label}, + $cgi->img({-src => esc_url($logo), + -width => 72, -height => 27, + -alt => "git", + -class => "logo"})); + } + print_nav_breadcrumbs(%opts); + print "
\n"; + + my $have_search = gitweb_check_feature('search'); + if (defined $project && $have_search) { + print_search_form(); + } +} + +sub git_footer_html { + my $feed_class = 'rss_logo'; + + print "
\n"; + if (defined $project) { + my $descr = git_get_project_description($project); + if (defined $descr) { + print "\n"; + } + + my %href_params = get_feed_info(); + if (!%href_params) { + $feed_class .= ' generic'; + } + $href_params{'-title'} ||= 'log'; + + foreach my $format (qw(RSS Atom)) { + $href_params{'action'} = lc($format); + print $cgi->a({-href => href(%href_params), + -title => "$href_params{'-title'} $format feed", + -class => $feed_class}, $format)."\n"; + } + + } else { + print $cgi->a({-href => href(project=>undef, action=>"opml", + project_filter => $project_filter), + -class => $feed_class}, "OPML") . " "; + print $cgi->a({-href => href(project=>undef, action=>"project_index", + project_filter => $project_filter), + -class => $feed_class}, "TXT") . "\n"; + } + print "
\n"; # class="page_footer" + + if (defined $t0 && gitweb_check_feature('timed')) { + print "
\n"; + print 'This page took '. + ''. + tv_interval($t0, [ gettimeofday() ]). + ' seconds '. + ' and '. + ''. + $number_of_git_cmds. + ' git commands '. + " to generate.\n"; + print "
\n"; # class="page_footer" + } + + if (defined $site_footer && -f $site_footer) { + insert_file($site_footer); + } + + print qq!\n!; + if (defined $action && + $action eq 'blame_incremental') { + print qq!\n!; + } else { + my ($jstimezone, $tz_cookie, $datetime_class) = + gitweb_get_feature('javascript-timezone'); + + print qq!\n!; + } + + print "\n" . + ""; +} + +# die_error(, [, ]) +# Example: die_error(404, 'Hash not found') +# By convention, use the following status codes (as defined in RFC 2616): +# 400: Invalid or missing CGI parameters, or +# requested object exists but has wrong type. +# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on +# this server or project. +# 404: Requested object/revision/project doesn't exist. +# 500: The server isn't configured properly, or +# an internal error occurred (e.g. failed assertions caused by bugs), or +# an unknown error occurred (e.g. the git binary died unexpectedly). +# 503: The server is currently unavailable (because it is overloaded, +# or down for maintenance). Generally, this is a temporary state. +sub die_error { + my $status = shift || 500; + my $error = esc_html(shift) || "Internal Server Error"; + my $extra = shift; + my %opts = @_; + + my %http_responses = ( + 400 => '400 Bad Request', + 403 => '403 Forbidden', + 404 => '404 Not Found', + 500 => '500 Internal Server Error', + 503 => '503 Service Unavailable', + ); + git_header_html($http_responses{$status}, undef, %opts); + print < +

+$status - $error +
+EOF + if (defined $extra) { + print "
\n" . + "$extra\n"; + } + print "\n"; + + git_footer_html(); + goto DONE_GITWEB + unless ($opts{'-error_handler'}); +} + +## ---------------------------------------------------------------------- +## functions printing or outputting HTML: navigation + +sub git_print_page_nav { + my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_; + $extra = '' if !defined $extra; # pager or formats + + my @navs = qw(summary shortlog log commit commitdiff tree); + if ($suppress) { + @navs = grep { $_ ne $suppress } @navs; + } + + my %arg = map { $_ => {action=>$_} } @navs; + if (defined $head) { + for (qw(commit commitdiff)) { + $arg{$_}{'hash'} = $head; + } + if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) { + for (qw(shortlog log)) { + $arg{$_}{'hash'} = $head; + } + } + } + + $arg{'tree'}{'hash'} = $treehead if defined $treehead; + $arg{'tree'}{'hash_base'} = $treebase if defined $treebase; + + my @actions = gitweb_get_feature('actions'); + my %repl = ( + '%' => '%', + 'n' => $project, # project name + 'f' => $git_dir, # project path within filesystem + 'h' => $treehead || '', # current hash ('h' parameter) + 'b' => $treebase || '', # hash base ('hb' parameter) + ); + while (@actions) { + my ($label, $link, $pos) = splice(@actions,0,3); + # insert + @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs; + # munch munch + $link =~ s/%([%nfhb])/$repl{$1}/g; + $arg{$label}{'_href'} = $link; + } + + print "
\n" . + (join " | ", + map { $_ eq $current ? + $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_") + } @navs); + print "
\n$extra
\n" . + "
\n"; +} + +# returns a submenu for the navigation of the refs views (tags, heads, +# remotes) with the current view disabled and the remotes view only +# available if the feature is enabled +sub format_ref_views { + my ($current) = @_; + my @ref_views = qw{tags heads}; + push @ref_views, 'remotes' if gitweb_check_feature('remote_heads'); + return join " | ", map { + $_ eq $current ? $_ : + $cgi->a({-href => href(action=>$_)}, $_) + } @ref_views +} + +sub format_paging_nav { + my ($action, $page, $has_next_link) = @_; + my $paging_nav; + + + if ($page > 0) { + $paging_nav .= + $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") . + " ⋅ " . + $cgi->a({-href => href(-replay=>1, page=>$page-1), + -accesskey => "p", -title => "Alt-p"}, "prev"); + } else { + $paging_nav .= "first ⋅ prev"; + } + + if ($has_next_link) { + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(-replay=>1, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } else { + $paging_nav .= " ⋅ next"; + } + + return $paging_nav; +} + +## ...................................................................... +## functions printing or outputting HTML: div + +sub git_print_header_div { + my ($action, $title, $hash, $hash_base) = @_; + my %args = (); + + $args{'action'} = $action; + $args{'hash'} = $hash if $hash; + $args{'hash_base'} = $hash_base if $hash_base; + + print "
\n" . + $cgi->a({-href => href(%args), -class => "title"}, + $title ? $title : $action) . + "\n
\n"; +} + +sub format_repo_url { + my ($name, $url) = @_; + return "$name$url\n"; +} + +# Group output by placing it in a DIV element and adding a header. +# Options for start_div() can be provided by passing a hash reference as the +# first parameter to the function. +# Options to git_print_header_div() can be provided by passing an array +# reference. This must follow the options to start_div if they are present. +# The content can be a scalar, which is output as-is, a scalar reference, which +# is output after html escaping, an IO handle passed either as *handle or +# *handle{IO}, or a function reference. In the latter case all following +# parameters will be taken as argument to the content function call. +sub git_print_section { + my ($div_args, $header_args, $content); + my $arg = shift; + if (ref($arg) eq 'HASH') { + $div_args = $arg; + $arg = shift; + } + if (ref($arg) eq 'ARRAY') { + $header_args = $arg; + $arg = shift; + } + $content = $arg; + + print $cgi->start_div($div_args); + git_print_header_div(@$header_args); + + if (ref($content) eq 'CODE') { + $content->(@_); + } elsif (ref($content) eq 'SCALAR') { + print esc_html($$content); + } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') { + print <$content>; + } elsif (!ref($content) && defined($content)) { + print $content; + } + + print $cgi->end_div; +} + +sub format_timestamp_html { + my $date = shift; + my $strtime = $date->{'rfc2822'}; + + my (undef, undef, $datetime_class) = + gitweb_get_feature('javascript-timezone'); + if ($datetime_class) { + $strtime = qq!$strtime!; + } + + my $localtime_format = '(%02d:%02d %s)'; + if ($date->{'hour_local'} < 6) { + $localtime_format = '(%02d:%02d %s)'; + } + $strtime .= ' ' . + sprintf($localtime_format, + $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'}); + + return $strtime; +} + +# Outputs the author name and date in long form +sub git_print_authorship { + my $co = shift; + my %opts = @_; + my $tag = $opts{-tag} || 'div'; + my $author = $co->{'author_name'}; + + my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'}); + print "<$tag class=\"author_date\">" . + format_search_author($author, "author", esc_html($author)) . + " [".format_timestamp_html(\%ad)."]". + git_get_avatar($co->{'author_email'}, -pad_before => 1) . + "\n"; +} + +# Outputs table rows containing the full author or committer information, +# in the format expected for 'commit' view (& similar). +# Parameters are a commit hash reference, followed by the list of people +# to output information for. If the list is empty it defaults to both +# author and committer. +sub git_print_authorship_rows { + my $co = shift; + # too bad we can't use @people = @_ || ('author', 'committer') + my @people = @_; + @people = ('author', 'committer') unless @people; + foreach my $who (@people) { + my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"}); + print "$who" . + format_search_author($co->{"${who}_name"}, $who, + esc_html($co->{"${who}_name"})) . " " . + format_search_author($co->{"${who}_email"}, $who, + esc_html("<" . $co->{"${who}_email"} . ">")) . + "" . + git_get_avatar($co->{"${who}_email"}, -size => 'double') . + "\n" . + "" . + "" . + format_timestamp_html(\%wd) . + "" . + "\n"; + } +} + +sub git_print_page_path { + my $name = shift; + my $type = shift; + my $hb = shift; + + + print "
"; + print $cgi->a({-href => href(action=>"tree", hash_base=>$hb), + -title => 'tree root'}, to_utf8("[$project]")); + print " / "; + if (defined $name) { + my @dirname = split '/', $name; + my $basename = pop @dirname; + my $fullname = ''; + + foreach my $dir (@dirname) { + $fullname .= ($fullname ? '/' : '') . $dir; + print $cgi->a({-href => href(action=>"tree", file_name=>$fullname, + hash_base=>$hb), + -title => $fullname}, esc_path($dir)); + print " / "; + } + if (defined $type && $type eq 'blob') { + print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name, + hash_base=>$hb), + -title => $name}, esc_path($basename)); + } elsif (defined $type && $type eq 'tree') { + print $cgi->a({-href => href(action=>"tree", file_name=>$file_name, + hash_base=>$hb), + -title => $name}, esc_path($basename)); + print " / "; + } else { + print esc_path($basename); + } + } + print "
\n"; +} + +sub git_print_log { + my $log = shift; + my %opts = @_; + + if ($opts{'-remove_title'}) { + # remove title, i.e. first line of log + shift @$log; + } + # remove leading empty lines + while (defined $log->[0] && $log->[0] eq "") { + shift @$log; + } + + # print log + my $skip_blank_line = 0; + foreach my $line (@$log) { + if ($line =~ m/^\s*([A-Z][-A-Za-z]*-([Bb]y|[Tt]o)|C[Cc]|(Clos|Fix)es): /) { + if (! $opts{'-remove_signoff'}) { + print "" . esc_html($line) . "
\n"; + $skip_blank_line = 1; + } + next; + } + + if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) { + if (! $opts{'-remove_signoff'}) { + print "" . esc_html($1) . ": " . + "" . esc_html($2) . "" . + "
\n"; + $skip_blank_line = 1; + } + next; + } + + # print only one empty line + # do not print empty line after signoff + if ($line eq "") { + next if ($skip_blank_line); + $skip_blank_line = 1; + } else { + $skip_blank_line = 0; + } + + print format_log_line_html($line) . "
\n"; + } + + if ($opts{'-final_empty_line'}) { + # end with single empty line + print "
\n" unless $skip_blank_line; + } +} + +# return link target (what link points to) +sub git_get_link_target { + my $hash = shift; + my $link_target; + + # read link + open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash + or return; + { + local $/ = undef; + $link_target = <$fd>; + } + close $fd + or return; + + return $link_target; +} + +# given link target, and the directory (basedir) the link is in, +# return target of link relative to top directory (top tree); +# return undef if it is not possible (including absolute links). +sub normalize_link_target { + my ($link_target, $basedir) = @_; + + # absolute symlinks (beginning with '/') cannot be normalized + return if (substr($link_target, 0, 1) eq '/'); + + # normalize link target to path from top (root) tree (dir) + my $path; + if ($basedir) { + $path = $basedir . '/' . $link_target; + } else { + # we are in top (root) tree (dir) + $path = $link_target; + } + + # remove //, /./, and /../ + my @path_parts; + foreach my $part (split('/', $path)) { + # discard '.' and '' + next if (!$part || $part eq '.'); + # handle '..' + if ($part eq '..') { + if (@path_parts) { + pop @path_parts; + } else { + # link leads outside repository (outside top dir) + return; + } + } else { + push @path_parts, $part; + } + } + $path = join('/', @path_parts); + + return $path; +} + +# print tree entry (row of git_tree), but without encompassing element +sub git_print_tree_entry { + my ($t, $basedir, $hash_base, $have_blame) = @_; + + my %base_key = (); + $base_key{'hash_base'} = $hash_base if defined $hash_base; + + # The format of a table row is: mode list link. Where mode is + # the mode of the entry, list is the name of the entry, an href, + # and link is the action links of the entry. + + print "" . mode_str($t->{'mode'}) . "\n"; + if (exists $t->{'size'}) { + print "$t->{'size'}\n"; + } + if ($t->{'type'} eq "blob") { + print "" . + $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key), + -class => "list"}, esc_path($t->{'name'})); + if (S_ISLNK(oct $t->{'mode'})) { + my $link_target = git_get_link_target($t->{'hash'}); + if ($link_target) { + my $norm_target = normalize_link_target($link_target, $basedir); + if (defined $norm_target) { + print " -> " . + $cgi->a({-href => href(action=>"object", hash_base=>$hash_base, + file_name=>$norm_target), + -title => $norm_target}, esc_path($link_target)); + } else { + print " -> " . esc_path($link_target); + } + } + } + print "\n"; + print ""; + print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + "blob"); + if ($have_blame) { + print " | " . + $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + "blame"); + } + if (defined $hash_base) { + print " | " . + $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")}, + "history"); + } + print " | " . + $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base, + file_name=>"$basedir$t->{'name'}")}, + "raw"); + print "\n"; + + } elsif ($t->{'type'} eq "tree") { + print ""; + print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", + %base_key)}, + esc_path($t->{'name'})); + print "\n"; + print ""; + print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", + %base_key)}, + "tree"); + if (defined $hash_base) { + print " | " . + $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + file_name=>"$basedir$t->{'name'}")}, + "history"); + } + print "\n"; + } else { + # unknown object: we can only present history for it + # (this includes 'commit' object, i.e. submodule support) + print "" . + esc_path($t->{'name'}) . + "\n"; + print ""; + if (defined $hash_base) { + print $cgi->a({-href => href(action=>"history", + hash_base=>$hash_base, + file_name=>"$basedir$t->{'name'}")}, + "history"); + } + print "\n"; + } +} + +## ...................................................................... +## functions printing large fragments of HTML + +# get pre-image filenames for merge (combined) diff +sub fill_from_file_info { + my ($diff, @parents) = @_; + + $diff->{'from_file'} = [ ]; + $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef; + for (my $i = 0; $i < $diff->{'nparents'}; $i++) { + if ($diff->{'status'}[$i] eq 'R' || + $diff->{'status'}[$i] eq 'C') { + $diff->{'from_file'}[$i] = + git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]); + } + } + + return $diff; +} + +# is current raw difftree line of file deletion +sub is_deleted { + my $diffinfo = shift; + + return $diffinfo->{'to_id'} eq ('0' x 40) || $diffinfo->{'to_id'} eq ('0' x 64); +} + +# does patch correspond to [previous] difftree raw line +# $diffinfo - hashref of parsed raw diff format +# $patchinfo - hashref of parsed patch diff format +# (the same keys as in $diffinfo) +sub is_patch_split { + my ($diffinfo, $patchinfo) = @_; + + return defined $diffinfo && defined $patchinfo + && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'}; +} + + +sub git_difftree_body { + my ($difftree, $hash, @parents) = @_; + my ($parent) = $parents[0]; + my $have_blame = gitweb_check_feature('blame'); + print "
\n"; + if ($#{$difftree} > 10) { + print(($#{$difftree} + 1) . " files changed:\n"); + } + print "
\n"; + + print " 1 ? "combined " : "") . + "diff_tree\">\n"; + + # header only for combined diff in 'commitdiff' view + my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff'; + if ($has_header) { + # table header + print "\n" . + "\n"; # filename, patchN link + for (my $i = 0; $i < @parents; $i++) { + my $par = $parents[$i]; + print "\n"; + } + print "\n\n"; + } + + my $alternate = 1; + my $patchno = 0; + foreach my $line (@{$difftree}) { + my $diff = parsed_difftree_line($line); + + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + + if (exists $diff->{'nparents'}) { # combined diff + + fill_from_file_info($diff, @parents) + unless exists $diff->{'from_file'}; + + if (!is_deleted($diff)) { + # file exists in the result (child) commit + print "\n"; + } else { + print "\n"; + } + + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print "\n"; + } + + my $has_history = 0; + my $not_deleted = 0; + for (my $i = 0; $i < $diff->{'nparents'}; $i++) { + my $hash_parent = $parents[$i]; + my $from_hash = $diff->{'from_id'}[$i]; + my $from_path = $diff->{'from_file'}[$i]; + my $status = $diff->{'status'}[$i]; + + $has_history ||= ($status ne 'A'); + $not_deleted ||= ($status ne 'D'); + + if ($status eq 'A') { + print "\n"; + } elsif ($status eq 'D') { + print "\n"; + } else { + if ($diff->{'to_id'} eq $from_hash) { + print "\n"; + } + } + + print "\n"; + + print "\n"; + next; # instead of 'else' clause, to avoid extra indent + } + # else ordinary diff + + my ($to_mode_oct, $to_mode_str, $to_file_type); + my ($from_mode_oct, $from_mode_str, $from_file_type); + if ($diff->{'to_mode'} ne ('0' x 6)) { + $to_mode_oct = oct $diff->{'to_mode'}; + if (S_ISREG($to_mode_oct)) { # only for regular file + $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits + } + $to_file_type = file_type($diff->{'to_mode'}); + } + if ($diff->{'from_mode'} ne ('0' x 6)) { + $from_mode_oct = oct $diff->{'from_mode'}; + if (S_ISREG($from_mode_oct)) { # only for regular file + $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits + } + $from_file_type = file_type($diff->{'from_mode'}); + } + + if ($diff->{'status'} eq "A") { # created + my $mode_chng = "[new $to_file_type"; + $mode_chng .= " with mode: $to_mode_str" if $to_mode_str; + $mode_chng .= "]"; + print "\n"; + print "\n"; + print "\n"; + + } elsif ($diff->{'status'} eq "D") { # deleted + my $mode_chng = "[deleted $from_file_type]"; + print "\n"; + print "\n"; + print "\n"; + + } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed + my $mode_chnge = ""; + if ($diff->{'from_mode'} != $diff->{'to_mode'}) { + $mode_chnge = "[changed"; + if ($from_file_type ne $to_file_type) { + $mode_chnge .= " from $from_file_type to $to_file_type"; + } + if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) { + if ($from_mode_str && $to_mode_str) { + $mode_chnge .= " mode: $from_mode_str->$to_mode_str"; + } elsif ($to_mode_str) { + $mode_chnge .= " mode: $to_mode_str"; + } + } + $mode_chnge .= "]\n"; + } + print "\n"; + print "\n"; + print "\n"; + + } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied + my %status_name = ('R' => 'moved', 'C' => 'copied'); + my $nstatus = $status_name{$diff->{'status'}}; + my $mode_chng = ""; + if ($diff->{'from_mode'} != $diff->{'to_mode'}) { + # mode also for directories, so we cannot use $to_mode_str + $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777); + } + print "\n" . + "\n" . + "\n"; + + } # we should not encounter Unmerged (U) or Unknown (X) status + print "\n"; + } + print "" if $has_header; + print "
" . + $cgi->a({-href => href(action=>"commitdiff", + hash=>$hash, hash_parent=>$par), + -title => 'commitdiff to parent number ' . + ($i+1) . ': ' . substr($par,0,7)}, + $i+1) . + " 
" . + $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + file_name=>$diff->{'to_file'}, + hash_base=>$hash), + -class => "list"}, esc_path($diff->{'to_file'})) . + "" . + esc_path($diff->{'to_file'}) . + "" . + $cgi->a({-href => href(-anchor=>"patch$patchno")}, + "patch") . + " | " . + " | " . + $cgi->a({-href => href(action=>"blob", + hash_base=>$hash, + hash=>$from_hash, + file_name=>$from_path)}, + "blob" . ($i+1)) . + " | "; + } + print $cgi->a({-href => href(action=>"blobdiff", + hash=>$diff->{'to_id'}, + hash_parent=>$from_hash, + hash_base=>$hash, + hash_parent_base=>$hash_parent, + file_name=>$diff->{'to_file'}, + file_parent=>$from_path)}, + "diff" . ($i+1)) . + " | "; + if ($not_deleted) { + print $cgi->a({-href => href(action=>"blob", + hash=>$diff->{'to_id'}, + file_name=>$diff->{'to_file'}, + hash_base=>$hash)}, + "blob"); + print " | " if ($has_history); + } + if ($has_history) { + print $cgi->a({-href => href(action=>"history", + file_name=>$diff->{'to_file'}, + hash_base=>$hash)}, + "history"); + } + print "
"; + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + hash_base=>$hash, file_name=>$diff->{'file'}), + -class => "list"}, esc_path($diff->{'file'})); + print "$mode_chng"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => href(-anchor=>"patch$patchno")}, + "patch") . + " | "; + } + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + hash_base=>$hash, file_name=>$diff->{'file'})}, + "blob"); + print ""; + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'}, + hash_base=>$parent, file_name=>$diff->{'file'}), + -class => "list"}, esc_path($diff->{'file'})); + print "$mode_chng"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => href(-anchor=>"patch$patchno")}, + "patch") . + " | "; + } + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'}, + hash_base=>$parent, file_name=>$diff->{'file'})}, + "blob") . " | "; + if ($have_blame) { + print $cgi->a({-href => href(action=>"blame", hash_base=>$parent, + file_name=>$diff->{'file'})}, + "blame") . " | "; + } + print $cgi->a({-href => href(action=>"history", hash_base=>$parent, + file_name=>$diff->{'file'})}, + "history"); + print ""; + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + hash_base=>$hash, file_name=>$diff->{'file'}), + -class => "list"}, esc_path($diff->{'file'})); + print "$mode_chnge"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => href(-anchor=>"patch$patchno")}, + "patch") . + " | "; + } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) { + # "commit" view and modified file (not onlu mode changed) + print $cgi->a({-href => href(action=>"blobdiff", + hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'}, + hash_base=>$hash, hash_parent_base=>$parent, + file_name=>$diff->{'file'})}, + "diff") . + " | "; + } + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + hash_base=>$hash, file_name=>$diff->{'file'})}, + "blob") . " | "; + if ($have_blame) { + print $cgi->a({-href => href(action=>"blame", hash_base=>$hash, + file_name=>$diff->{'file'})}, + "blame") . " | "; + } + print $cgi->a({-href => href(action=>"history", hash_base=>$hash, + file_name=>$diff->{'file'})}, + "history"); + print "" . + $cgi->a({-href => href(action=>"blob", hash_base=>$hash, + hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}), + -class => "list"}, esc_path($diff->{'to_file'})) . "[$nstatus from " . + $cgi->a({-href => href(action=>"blob", hash_base=>$parent, + hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}), + -class => "list"}, esc_path($diff->{'from_file'})) . + " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => href(-anchor=>"patch$patchno")}, + "patch") . + " | "; + } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) { + # "commit" view and modified file (not only pure rename or copy) + print $cgi->a({-href => href(action=>"blobdiff", + hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'}, + hash_base=>$hash, hash_parent_base=>$parent, + file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})}, + "diff") . + " | "; + } + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + hash_base=>$parent, file_name=>$diff->{'to_file'})}, + "blob") . " | "; + if ($have_blame) { + print $cgi->a({-href => href(action=>"blame", hash_base=>$hash, + file_name=>$diff->{'to_file'})}, + "blame") . " | "; + } + print $cgi->a({-href => href(action=>"history", hash_base=>$hash, + file_name=>$diff->{'to_file'})}, + "history"); + print "
\n"; +} + +# Print context lines and then rem/add lines in a side-by-side manner. +sub print_sidebyside_diff_lines { + my ($ctx, $rem, $add) = @_; + + # print context block before add/rem block + if (@$ctx) { + print join '', + '
', + '
', + @$ctx, + '
', + '
', + @$ctx, + '
', + '
'; + } + + if (!@$add) { + # pure removal + print join '', + '
', + '
', + @$rem, + '
', + '
'; + } elsif (!@$rem) { + # pure addition + print join '', + '
', + '
', + @$add, + '
', + '
'; + } else { + print join '', + '
', + '
', + @$rem, + '
', + '
', + @$add, + '
', + '
'; + } +} + +# Print context lines and then rem/add lines in inline manner. +sub print_inline_diff_lines { + my ($ctx, $rem, $add) = @_; + + print @$ctx, @$rem, @$add; +} + +# Format removed and added line, mark changed part and HTML-format them. +# Implementation is based on contrib/diff-highlight +sub format_rem_add_lines_pair { + my ($rem, $add, $num_parents) = @_; + + # We need to untabify lines before split()'ing them; + # otherwise offsets would be invalid. + chomp $rem; + chomp $add; + $rem = untabify($rem); + $add = untabify($add); + + my @rem = split(//, $rem); + my @add = split(//, $add); + my ($esc_rem, $esc_add); + # Ignore leading +/- characters for each parent. + my ($prefix_len, $suffix_len) = ($num_parents, 0); + my ($prefix_has_nonspace, $suffix_has_nonspace); + + my $shorter = (@rem < @add) ? @rem : @add; + while ($prefix_len < $shorter) { + last if ($rem[$prefix_len] ne $add[$prefix_len]); + + $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/); + $prefix_len++; + } + + while ($prefix_len + $suffix_len < $shorter) { + last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]); + + $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/); + $suffix_len++; + } + + # Mark lines that are different from each other, but have some common + # part that isn't whitespace. If lines are completely different, don't + # mark them because that would make output unreadable, especially if + # diff consists of multiple lines. + if ($prefix_has_nonspace || $suffix_has_nonspace) { + $esc_rem = esc_html_hl_regions($rem, 'marked', + [$prefix_len, @rem - $suffix_len], -nbsp=>1); + $esc_add = esc_html_hl_regions($add, 'marked', + [$prefix_len, @add - $suffix_len], -nbsp=>1); + } else { + $esc_rem = esc_html($rem, -nbsp=>1); + $esc_add = esc_html($add, -nbsp=>1); + } + + return format_diff_line(\$esc_rem, 'rem'), + format_diff_line(\$esc_add, 'add'); +} + +# HTML-format diff context, removed and added lines. +sub format_ctx_rem_add_lines { + my ($ctx, $rem, $add, $num_parents) = @_; + my (@new_ctx, @new_rem, @new_add); + my $can_highlight = 0; + my $is_combined = ($num_parents > 1); + + # Highlight if every removed line has a corresponding added line. + if (@$add > 0 && @$add == @$rem) { + $can_highlight = 1; + + # Highlight lines in combined diff only if the chunk contains + # diff between the same version, e.g. + # + # - a + # - b + # + c + # + d + # + # Otherwise the highlighting would be confusing. + if ($is_combined) { + for (my $i = 0; $i < @$add; $i++) { + my $prefix_rem = substr($rem->[$i], 0, $num_parents); + my $prefix_add = substr($add->[$i], 0, $num_parents); + + $prefix_rem =~ s/-/+/g; + + if ($prefix_rem ne $prefix_add) { + $can_highlight = 0; + last; + } + } + } + } + + if ($can_highlight) { + for (my $i = 0; $i < @$add; $i++) { + my ($line_rem, $line_add) = format_rem_add_lines_pair( + $rem->[$i], $add->[$i], $num_parents); + push @new_rem, $line_rem; + push @new_add, $line_add; + } + } else { + @new_rem = map { format_diff_line($_, 'rem') } @$rem; + @new_add = map { format_diff_line($_, 'add') } @$add; + } + + @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx; + + return (\@new_ctx, \@new_rem, \@new_add); +} + +# Print context lines and then rem/add lines. +sub print_diff_lines { + my ($ctx, $rem, $add, $diff_style, $num_parents) = @_; + my $is_combined = $num_parents > 1; + + ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add, + $num_parents); + + if ($diff_style eq 'sidebyside' && !$is_combined) { + print_sidebyside_diff_lines($ctx, $rem, $add); + } else { + # default 'inline' style and unknown styles + print_inline_diff_lines($ctx, $rem, $add); + } +} + +sub print_diff_chunk { + my ($diff_style, $num_parents, $from, $to, @chunk) = @_; + my (@ctx, @rem, @add); + + # The class of the previous line. + my $prev_class = ''; + + return unless @chunk; + + # incomplete last line might be among removed or added lines, + # or both, or among context lines: find which + for (my $i = 1; $i < @chunk; $i++) { + if ($chunk[$i][0] eq 'incomplete') { + $chunk[$i][0] = $chunk[$i-1][0]; + } + } + + # guardian + push @chunk, ["", ""]; + + foreach my $line_info (@chunk) { + my ($class, $line) = @$line_info; + + # print chunk headers + if ($class && $class eq 'chunk_header') { + print format_diff_line($line, $class, $from, $to); + next; + } + + ## print from accumulator when have some add/rem lines or end + # of chunk (flush context lines), or when have add and rem + # lines and new block is reached (otherwise add/rem lines could + # be reordered) + if (!$class || ((@rem || @add) && $class eq 'ctx') || + (@rem && @add && $class ne $prev_class)) { + print_diff_lines(\@ctx, \@rem, \@add, + $diff_style, $num_parents); + @ctx = @rem = @add = (); + } + + ## adding lines to accumulator + # guardian value + last unless $line; + # rem, add or change + if ($class eq 'rem') { + push @rem, $line; + } elsif ($class eq 'add') { + push @add, $line; + } + # context line + if ($class eq 'ctx') { + push @ctx, $line; + } + + $prev_class = $class; + } +} + +sub git_patchset_body { + my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_; + my ($hash_parent) = $hash_parents[0]; + + my $is_combined = (@hash_parents > 1); + my $patch_idx = 0; + my $patch_number = 0; + my $patch_line; + my $diffinfo; + my $to_name; + my (%from, %to); + my @chunk; # for side-by-side diff + + print "
\n"; + + # skip to first patch + while ($patch_line = <$fd>) { + chomp $patch_line; + + last if ($patch_line =~ m/^diff /); + } + + PATCH: + while ($patch_line) { + + # parse "git diff" header line + if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) { + # $1 is from_name, which we do not use + $to_name = unquote($2); + $to_name =~ s!^b/!!; + } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) { + # $1 is 'cc' or 'combined', which we do not use + $to_name = unquote($2); + } else { + $to_name = undef; + } + + # check if current patch belong to current raw line + # and parse raw git-diff line if needed + if (is_patch_split($diffinfo, { 'to_file' => $to_name })) { + # this is continuation of a split patch + print "
\n"; + } else { + # advance raw git-diff output if needed + $patch_idx++ if defined $diffinfo; + + # read and prepare patch information + $diffinfo = parsed_difftree_line($difftree->[$patch_idx]); + + # compact combined diff output can have some patches skipped + # find which patch (using pathname of result) we are at now; + if ($is_combined) { + while ($to_name ne $diffinfo->{'to_file'}) { + print "
\n" . + format_diff_cc_simplified($diffinfo, @hash_parents) . + "
\n"; # class="patch" + + $patch_idx++; + $patch_number++; + + last if $patch_idx > $#$difftree; + $diffinfo = parsed_difftree_line($difftree->[$patch_idx]); + } + } + + # modifies %from, %to hashes + parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents); + + # this is first patch for raw difftree line with $patch_idx index + # we index @$difftree array from 0, but number patches from 1 + print "
\n"; + } + + # git diff header + #assert($patch_line =~ m/^diff /) if DEBUG; + #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed + $patch_number++; + # print "git diff" header + print format_git_diff_header_line($patch_line, $diffinfo, + \%from, \%to); + + # print extended diff header + print "
\n"; + EXTENDED_HEADER: + while ($patch_line = <$fd>) { + chomp $patch_line; + + last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /); + + print format_extended_diff_header_line($patch_line, $diffinfo, + \%from, \%to); + } + print "
\n"; # class="diff extended_header" + + # from-file/to-file diff header + if (! $patch_line) { + print "
\n"; # class="patch" + last PATCH; + } + next PATCH if ($patch_line =~ m/^diff /); + #assert($patch_line =~ m/^---/) if DEBUG; + + my $last_patch_line = $patch_line; + $patch_line = <$fd>; + chomp $patch_line; + #assert($patch_line =~ m/^\+\+\+/) if DEBUG; + + print format_diff_from_to_header($last_patch_line, $patch_line, + $diffinfo, \%from, \%to, + @hash_parents); + + # the patch itself + LINE: + while ($patch_line = <$fd>) { + chomp $patch_line; + + next PATCH if ($patch_line =~ m/^diff /); + + my $class = diff_line_class($patch_line, \%from, \%to); + + if ($class eq 'chunk_header') { + print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk); + @chunk = (); + } + + push @chunk, [ $class, $patch_line ]; + } + + } continue { + if (@chunk) { + print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk); + @chunk = (); + } + print "
\n"; # class="patch" + } + + # for compact combined (--cc) format, with chunk and patch simplification + # the patchset might be empty, but there might be unprocessed raw lines + for (++$patch_idx if $patch_number > 0; + $patch_idx < @$difftree; + ++$patch_idx) { + # read and prepare patch information + $diffinfo = parsed_difftree_line($difftree->[$patch_idx]); + + # generate anchor for "patch" links in difftree / whatchanged part + print "
\n" . + format_diff_cc_simplified($diffinfo, @hash_parents) . + "
\n"; # class="patch" + + $patch_number++; + } + + if ($patch_number == 0) { + if (@hash_parents > 1) { + print "
Trivial merge
\n"; + } else { + print "
No differences found
\n"; + } + } + + print "
\n"; # class="patchset" +} + +# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + +sub git_project_search_form { + my ($searchtext, $search_use_regexp) = @_; + + my $limit = ''; + if ($project_filter) { + $limit = " in '$project_filter/'"; + } + + print "
\n"; + print $cgi->start_form(-method => 'get', -action => $my_uri) . + $cgi->hidden(-name => 'a', -value => 'project_list') . "\n"; + print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n" + if (defined $project_filter); + print $cgi->textfield(-name => 's', -value => $searchtext, + -title => "Search project by name and description$limit", + -size => 60) . "\n" . + "" . + $cgi->checkbox(-name => 'sr', -value => 1, -label => 're', + -checked => $search_use_regexp) . + "\n" . + $cgi->submit(-name => 'btnS', -value => 'Search') . + $cgi->end_form() . "\n" . + $cgi->a({-href => href(project => undef, searchtext => undef, + project_filter => $project_filter)}, + esc_html("List all projects$limit")) . "
\n"; + print "
\n"; +} + +# entry for given @keys needs filling if at least one of keys in list +# is not present in %$project_info +sub project_info_needs_filling { + my ($project_info, @keys) = @_; + + # return List::MoreUtils::any { !exists $project_info->{$_} } @keys; + foreach my $key (@keys) { + if (!exists $project_info->{$key}) { + return 1; + } + } + return; +} + +# fills project list info (age, description, owner, category, forks, etc.) +# for each project in the list, removing invalid projects from +# returned list, or fill only specified info. +# +# Invalid projects are removed from the returned list if and only if you +# ask 'age' or 'age_string' to be filled, because they are the only fields +# that run unconditionally git command that requires repository, and +# therefore do always check if project repository is invalid. +# +# USAGE: +# * fill_project_list_info(\@project_list, 'descr_long', 'ctags') +# ensures that 'descr_long' and 'ctags' fields are filled +# * @project_list = fill_project_list_info(\@project_list) +# ensures that all fields are filled (and invalid projects removed) +# +# NOTE: modifies $projlist, but does not remove entries from it +sub fill_project_list_info { + my ($projlist, @wanted_keys) = @_; + my @projects; + my $filter_set = sub { return @_; }; + if (@wanted_keys) { + my %wanted_keys = map { $_ => 1 } @wanted_keys; + $filter_set = sub { return grep { $wanted_keys{$_} } @_; }; + } + + my $show_ctags = gitweb_check_feature('ctags'); + PROJECT: + foreach my $pr (@$projlist) { + if (project_info_needs_filling($pr, $filter_set->('age', 'age_string'))) { + my (@activity) = git_get_last_activity($pr->{'path'}); + unless (@activity) { + next PROJECT; + } + ($pr->{'age'}, $pr->{'age_string'}) = @activity; + } + if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) { + my $descr = git_get_project_description($pr->{'path'}) || ""; + $descr = to_utf8($descr); + $pr->{'descr_long'} = $descr; + $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5); + } + if (project_info_needs_filling($pr, $filter_set->('owner'))) { + $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || ""; + } + if ($show_ctags && + project_info_needs_filling($pr, $filter_set->('ctags'))) { + $pr->{'ctags'} = git_get_project_ctags($pr->{'path'}); + } + if ($projects_list_group_categories && + project_info_needs_filling($pr, $filter_set->('category'))) { + my $cat = git_get_project_category($pr->{'path'}) || + $project_list_default_category; + $pr->{'category'} = to_utf8($cat); + } + if (project_info_needs_filling($pr, $filter_set->('language'))) { + $pr->{'language'} = git_get_project_config_by_path($pr->{'path'}, 'language') || ""; + } + if (project_info_needs_filling($pr, $filter_set->('license'))) { + $pr->{'license'} = git_get_project_config_by_path($pr->{'path'}, 'license') || ""; + } + if (project_info_needs_filling($pr, $filter_set->('version'))) { + $pr->{'version'} = git_get_latest_tag($pr->{'path'}) || ""; + } + + push @projects, $pr; + } + + return @projects; +} + +sub sort_projects_list { + my ($projlist, $order) = @_; + + sub order_str { + my $key = shift; + return sub { $a->{$key} cmp $b->{$key} }; + } + + sub order_num_then_undef { + my $key = shift; + return sub { + defined $a->{$key} ? + (defined $b->{$key} ? $a->{$key} <=> $b->{$key} : -1) : + (defined $b->{$key} ? 1 : 0) + }; + } + + my %orderings = ( + project => order_str('path'), + descr => order_str('descr_long'), + owner => order_str('owner'), + age => order_num_then_undef('age'), + language => order_str('language'), + license => order_str('license'), + version => order_str('version'), + ); + + my $ordering = $orderings{$order}; + return defined $ordering ? sort $ordering @$projlist : @$projlist; +} + +# returns a hash of categories, containing the list of project +# belonging to each category +sub build_projlist_by_category { + my ($projlist, $from, $to) = @_; + my %categories; + + $from = 0 unless defined $from; + $to = $#$projlist if (!defined $to || $#$projlist < $to); + + for (my $i = $from; $i <= $to; $i++) { + my $pr = $projlist->[$i]; + push @{$categories{ $pr->{'category'} }}, $pr; + } + + return wantarray ? %categories : \%categories; +} + +# print 'sort by' element, generating 'sort by $name' replay link +# if that order is not selected +sub print_sort_th { + print format_sort_th(@_); +} + +sub format_sort_th { + my ($name, $order, $header) = @_; + my $sort_th = ""; + $header ||= ucfirst($name); + + if ($order eq $name) { + $sort_th .= "$header\n"; + } else { + $sort_th .= "" . + $cgi->a({-href => href(-replay=>1, order=>$name), + -class => "header"}, $header) . + "\n"; + } + + return $sort_th; +} + +# Helper function to get gitweb config for a specific project +sub git_get_project_config_by_path { + my ($project_path, $key) = @_; + + my $project_dir = "$projectroot/$project_path"; + return unless -d $project_dir; + + my $config_file = "$project_dir/config"; + return unless -f $config_file; + + # Read the config file directly + open(my $fh, '-|', git_cmd(), 'config', '-f', $config_file, '--get', "gitweb.$key") + or return; + my $value = <$fh>; + close($fh); + + chomp $value if defined $value; + return $value; +} + +sub git_project_list_rows { + my ($projlist, $from, $to, $check_forks) = @_; + + $from = 0 unless defined $from; + $to = $#$projlist if (!defined $to || $#$projlist < $to); + + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my $pr = $projlist->[$i]; + + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + + if ($check_forks) { + print ""; + if ($pr->{'forks'}) { + my $nforks = scalar @{$pr->{'forks'}}; + if ($nforks > 0) { + print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"), + -title => "$nforks forks"}, "+"); + } else { + print $cgi->span({-title => "$nforks forks"}, "+"); + } + } + print "\n"; + } + print "" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"), + -class => "list"}, + esc_html_match_hl($pr->{'path'}, $search_regexp)) . + "\n" . + "" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"), + -class => "list", + -title => $pr->{'descr_long'}}, + $search_regexp + ? esc_html_match_hl_chopped($pr->{'descr_long'}, + $pr->{'descr'}, $search_regexp) + : esc_html($pr->{'descr'})) . + "\n"; + print "" . esc_html($pr->{'version'} || '-') . "\n"; + print "" . esc_html($pr->{'license'} || '-') . "\n"; + print "" . esc_html($pr->{'language'} || '-') . "\n"; + unless ($omit_owner) { + print "" . chop_and_escape_str($pr->{'owner'}, 15) . "\n"; + } + unless ($omit_age_column) { + print "{'age'}) . "\">" . + (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "\n"; + } + print"" . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") . + ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') . + "\n" . + "\n"; + } +} + +sub git_project_list_body { + # actually uses global variable $project + my ($projlist, $order, $from, $to, $extra, $no_header) = @_; + my @projects = @$projlist; + + my $check_forks = gitweb_check_feature('forks'); + my $show_ctags = gitweb_check_feature('ctags'); + my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef; + $check_forks = undef + if ($tagfilter || $search_regexp); + + # filtering out forks before filling info allows to do less work + @projects = filter_forks_from_projects_list(\@projects) + if ($check_forks); + # search_projects_list pre-fills required info + @projects = search_projects_list(\@projects, + 'search_regexp' => $search_regexp, + 'tagfilter' => $tagfilter) + if ($tagfilter || $search_regexp); + # fill the rest + my @all_fields = ('descr', 'descr_long', 'ctags', 'category'); + push @all_fields, ('age', 'age_string') unless($omit_age_column); + push @all_fields, 'owner' unless($omit_owner); + push @all_fields, ('language', 'license', 'version'); + @projects = fill_project_list_info(\@projects, @all_fields); + + $order ||= $default_projects_order; + $from = 0 unless defined $from; + $to = $#projects if (!defined $to || $#projects < $to); + + # short circuit + if ($from > $to) { + print "
\n". + "No such projects found
\n". + "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects
\n". + "
\n
\n"; + return; + } + + @projects = sort_projects_list(\@projects, $order); + + if ($show_ctags) { + my $ctags = git_gather_all_ctags(\@projects); + my $cloud = git_populate_project_tagcloud($ctags); + print git_show_project_tagcloud($cloud, 64); + } + + print "\n"; + unless ($no_header) { + print "\n"; + if ($check_forks) { + print "\n"; + } + print_sort_th('project', $order, 'Project'); + print_sort_th('descr', $order, 'Description'); + print_sort_th('version', $order, 'Version'); + print_sort_th('license', $order, 'License'); + print_sort_th('language', $order, 'Language'); + print_sort_th('owner', $order, 'Owner') unless $omit_owner; + print_sort_th('age', $order, 'Last Change') unless $omit_age_column; + print "\n" . # for links + "\n"; + } + + if ($projects_list_group_categories) { + # only display categories with projects in the $from-$to window + @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to]; + my %categories = build_projlist_by_category(\@projects, $from, $to); + foreach my $cat (sort keys %categories) { + unless ($cat eq "") { + print "\n"; + if ($check_forks) { + print "\n"; + } + print "\n"; + print "\n"; + } + + git_project_list_rows($categories{$cat}, undef, undef, $check_forks); + } + } else { + git_project_list_rows(\@projects, $from, $to, $check_forks); + } + + if (defined $extra) { + print "\n"; + if ($check_forks) { + print "\n"; + } + print "\n" . + "\n"; + } + print "
".esc_html($cat)."
$extra
\n"; +} + +sub git_log_body { + # uses global variable $project + my ($commitlist, $from, $to, $refs, $extra) = @_; + + $from = 0 unless defined $from; + $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to); + + for (my $i = 0; $i <= $to; $i++) { + my %co = %{$commitlist->[$i]}; + next if !%co; + my $commit = $co{'id'}; + my $ref = format_ref_marker($refs, $commit); + git_print_header_div('commit', + "$co{'age_string'}" . + esc_html($co{'title'}) . $ref, + $commit); + print "
\n" . + "
\n" . + $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . + " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") . + "
\n" . + "
\n"; + git_print_authorship(\%co, -tag => 'span'); + print "
\n
\n"; + + print "
\n"; + git_print_log($co{'comment'}, -final_empty_line=> 1); + print "
\n"; + } + if ($extra) { + print "
\n"; + print "$extra\n"; + print "
\n"; + } +} + +sub git_shortlog_body { + # uses global variable $project + my ($commitlist, $from, $to, $refs, $extra) = @_; + + $from = 0 unless defined $from; + $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to); + + print "\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my %co = %{$commitlist->[$i]}; + my $commit = $co{'id'}; + my $ref = format_ref_marker($refs, $commit); + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + # git_summary() used print "\n" . + print "\n" . + format_author_html('td', \%co, 10) . "\n" . + "\n" . + "\n"; + } + if (defined $extra) { + print "\n" . + "\n" . + "\n"; + } + print "
$co{'age_string'}$co{'age_string_date'}"; + print format_subject_html($co{'title'}, $co{'title_short'}, + href(action=>"commit", hash=>$commit), $ref); + print "" . + $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " . + $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree"); + my $snapshot_links = format_snapshot_links($commit); + if (defined $snapshot_links) { + print " | " . $snapshot_links; + } + print "
$extra
\n"; +} + +sub git_history_body { + # Warning: assumes constant type (blob or tree) during history + my ($commitlist, $from, $to, $refs, $extra, + $file_name, $file_hash, $ftype) = @_; + + $from = 0 unless defined $from; + $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist}); + + print "\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my %co = %{$commitlist->[$i]}; + if (!%co) { + next; + } + my $commit = $co{'id'}; + + my $ref = format_ref_marker($refs, $commit); + + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + print "\n" . + # shortlog: format_author_html('td', \%co, 10) + format_author_html('td', \%co, 15, 3) . "\n" . + "\n" . + "\n"; + } + if (defined $extra) { + print "\n" . + "\n" . + "\n"; + } + print "
$co{'age_string_date'}"; + # originally git_history used chop_str($co{'title'}, 50) + print format_subject_html($co{'title'}, $co{'title_short'}, + href(action=>"commit", hash=>$commit), $ref); + print "" . + $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff"); + + if ($ftype eq 'blob') { + print " | " . + $cgi->a({-href => href(action=>"blob_plain", hash_base=>$commit, file_name=>$file_name)}, "raw"); + + my $blob_current = $file_hash; + my $blob_parent = git_get_hash_by_path($commit, $file_name); + if (defined $blob_current && defined $blob_parent && + $blob_current ne $blob_parent) { + print " | " . + $cgi->a({-href => href(action=>"blobdiff", + hash=>$blob_current, hash_parent=>$blob_parent, + hash_base=>$hash_base, hash_parent_base=>$commit, + file_name=>$file_name)}, + "diff to current"); + } + } + print "
$extra
\n"; +} + +sub git_tags_body { + # uses global variable $project + my ($taglist, $from, $to, $extra) = @_; + $from = 0 unless defined $from; + $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to); + + print "\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my $entry = $taglist->[$i]; + my %tag = %$entry; + my $comment = $tag{'subject'}; + my $comment_short; + if (defined $comment) { + $comment_short = chop_str($comment, 30, 5); + } + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + if (defined $tag{'age'}) { + print "\n"; + } else { + print "\n"; + } + print "\n" . + "\n" . + "\n" . + "\n" . + ""; + } + if (defined $extra) { + print "\n" . + "\n" . + "\n"; + } + print "
$tag{'age'}" . + $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}), + -class => "list name"}, esc_html($tag{'name'})) . + ""; + if (defined $comment) { + print format_subject_html($comment, $comment_short, + href(action=>"tag", hash=>$tag{'id'})); + } + print ""; + if ($tag{'type'} eq "tag") { + print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag"); + } else { + print " "; + } + print "" . " | " . + $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'}); + if ($tag{'reftype'} eq "commit") { + print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") . + " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log"); + } elsif ($tag{'reftype'} eq "blob") { + print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw"); + } + print "
$extra
\n"; +} + +sub git_heads_body { + # uses global variable $project + my ($headlist, $head_at, $from, $to, $extra) = @_; + $from = 0 unless defined $from; + $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to); + + print "\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my $entry = $headlist->[$i]; + my %ref = %$entry; + my $curr = defined $head_at && $ref{'id'} eq $head_at; + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + print "\n" . + ($curr ? "\n" . + "\n" . + ""; + } + if (defined $extra) { + print "\n" . + "\n" . + "\n"; + } + print "
$ref{'age'}" : "") . + $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}), + -class => "list name"},esc_html($ref{'name'})) . + "" . + $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " . + $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " . + $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") . + "
$extra
\n"; +} + +# Display a single remote block +sub git_remote_block { + my ($remote, $rdata, $limit, $head) = @_; + + my $heads = $rdata->{'heads'}; + my $fetch = $rdata->{'fetch'}; + my $push = $rdata->{'push'}; + + my $urls_table = "\n" ; + + if (defined $fetch) { + if ($fetch eq $push) { + $urls_table .= format_repo_url("URL", $fetch); + } else { + $urls_table .= format_repo_url("Fetch URL", $fetch); + $urls_table .= format_repo_url("Push URL", $push) if defined $push; + } + } elsif (defined $push) { + $urls_table .= format_repo_url("Push URL", $push); + } else { + $urls_table .= format_repo_url("", "No remote URL"); + } + + $urls_table .= "
\n"; + + my $dots; + if (defined $limit && $limit < @$heads) { + $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "..."); + } + + print $urls_table; + git_heads_body($heads, $head, 0, $limit, $dots); +} + +# Display a list of remote names with the respective fetch and push URLs +sub git_remotes_list { + my ($remotedata, $limit) = @_; + print "\n"; + my $alternate = 1; + my @remotes = sort keys %$remotedata; + + my $limited = $limit && $limit < @remotes; + + $#remotes = $limit - 1 if $limited; + + while (my $remote = shift @remotes) { + my $rdata = $remotedata->{$remote}; + my $fetch = $rdata->{'fetch'}; + my $push = $rdata->{'push'}; + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + print ""; + print ""; + + print "\n"; + } + + if ($limited) { + print "\n" . + "\n" . "\n"; + } + + print "
" . + $cgi->a({-href=> href(action=>'remotes', hash=>$remote), + -class=> "list name"},esc_html($remote)) . + "" . + (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") . + " | " . + (defined $push ? $cgi->a({-href=> $push}, "push") : "push") . + "
" . + $cgi->a({-href => href(action=>"remotes")}, "...") . + "
"; +} + +# Display remote heads grouped by remote, unless there are too many +# remotes, in which case we only display the remote names +sub git_remotes_body { + my ($remotedata, $limit, $head) = @_; + if ($limit and $limit < keys %$remotedata) { + git_remotes_list($remotedata, $limit); + } else { + fill_remote_heads($remotedata); + while (my ($remote, $rdata) = each %$remotedata) { + git_print_section({-class=>"remote", -id=>$remote}, + ["remotes", $remote, $remote], sub { + git_remote_block($remote, $rdata, $limit, $head); + }); + } + } +} + +sub git_search_message { + my %co = @_; + + my $greptype; + if ($searchtype eq 'commit') { + $greptype = "--grep="; + } elsif ($searchtype eq 'author') { + $greptype = "--author="; + } elsif ($searchtype eq 'committer') { + $greptype = "--committer="; + } + $greptype .= $searchtext; + my @commitlist = parse_commits($hash, 101, (100 * $page), undef, + $greptype, '--regexp-ignore-case', + $search_use_regexp ? '--extended-regexp' : '--fixed-strings'); + + my $paging_nav = ''; + if ($page > 0) { + $paging_nav .= + $cgi->a({-href => href(-replay=>1, page=>undef)}, + "first") . + " ⋅ " . + $cgi->a({-href => href(-replay=>1, page=>$page-1), + -accesskey => "p", -title => "Alt-p"}, "prev"); + } else { + $paging_nav .= "first ⋅ prev"; + } + my $next_link = ''; + if ($#commitlist >= 100) { + $next_link = + $cgi->a({-href => href(-replay=>1, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + $paging_nav .= " ⋅ $next_link"; + } else { + $paging_nav .= " ⋅ next"; + } + + git_header_html(); + + git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash); + if ($page == 0 && !@commitlist) { + print "

No match.

\n"; + } else { + git_search_grep_body(\@commitlist, 0, 99, $next_link); + } + + git_footer_html(); +} + +sub git_search_changes { + my %co = @_; + + local $/ = "\n"; + open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts, + '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext", + ($search_use_regexp ? '--pickaxe-regex' : ()) + or die_error(500, "Open git-log failed"); + + git_header_html(); + + git_print_page_nav('','', $hash,$co{'tree'},$hash); + git_print_header_div('commit', esc_html($co{'title'}), $hash); + + print "\n"; + my $alternate = 1; + undef %co; + my @files; + while (my $line = <$fd>) { + chomp $line; + next unless $line; + + my %set = parse_difftree_raw_line($line); + if (defined $set{'commit'}) { + # finish previous commit + if (%co) { + print "\n" . + "\n" . + "\n"; + } + + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + %co = parse_commit($set{'commit'}); + my $author = chop_and_escape_str($co{'author_name'}, 15, 5); + print "\n" . + "\n" . + "\n" . + "\n" . + "\n"; + } + + print "
" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, + "commit") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, + hash_base=>$co{'id'})}, + "tree") . + "
$co{'age_string_date'}$author" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), + -class => "list subject"}, + chop_and_escape_str($co{'title'}, 50) . "
"); + } elsif (defined $set{'to_id'}) { + next if is_deleted(\%set); + + print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'}, + hash=>$set{'to_id'}, file_name=>$set{'to_file'}), + -class => "list"}, + "" . esc_path($set{'file'}) . "") . + "
\n"; + } + } + close $fd; + + # finish last commit (warning: repetition!) + if (%co) { + print "
" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, + "commit") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, + hash_base=>$co{'id'})}, + "tree") . + "
\n"; + + git_footer_html(); +} + +sub git_search_files { + my %co = @_; + + local $/ = "\n"; + open my $fd, "-|", git_cmd(), 'grep', '-n', '-z', + $search_use_regexp ? ('-E', '-i') : '-F', + $searchtext, $co{'tree'} + or die_error(500, "Open git-grep failed"); + + git_header_html(); + + git_print_page_nav('','', $hash,$co{'tree'},$hash); + git_print_header_div('commit', esc_html($co{'title'}), $hash); + + print "\n"; + my $alternate = 1; + my $matches = 0; + my $lastfile = ''; + my $file_href; + while (my $line = <$fd>) { + chomp $line; + my ($file, $lno, $ltext, $binary); + last if ($matches++ > 1000); + if ($line =~ /^Binary file (.+) matches$/) { + $file = $1; + $binary = 1; + } else { + ($file, $lno, $ltext) = split(/\0/, $line, 3); + $file =~ s/^$co{'tree'}://; + } + if ($file ne $lastfile) { + $lastfile and print "\n"; + if ($alternate++) { + print "\n"; + } else { + print "\n"; + } + $file_href = href(action=>"blob", hash_base=>$co{'id'}, + file_name=>$file); + print "\n"; + if ($matches > 1000) { + print "
Too many matches, listing trimmed
\n"; + } + } else { + print "
No matches found
\n"; + } + close $fd; + + print "
". + $cgi->a({-href => $file_href, -class => "list"}, esc_path($file)); + print "\n"; + $lastfile = $file; + } + if ($binary) { + print "
Binary file
\n"; + } else { + $ltext = untabify($ltext); + if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) { + $ltext = esc_html($1, -nbsp=>1); + $ltext .= ''; + $ltext .= esc_html($2, -nbsp=>1); + $ltext .= ''; + $ltext .= esc_html($3, -nbsp=>1); + } else { + $ltext = esc_html($ltext, -nbsp=>1); + } + print "
" . + $cgi->a({-href => $file_href.'#l'.$lno, + -class => "linenr"}, sprintf('%4i', $lno)) . + ' ' . $ltext . "
\n"; + } + } + if ($lastfile) { + print "
\n"; + + git_footer_html(); +} + +sub git_search_grep_body { + my ($commitlist, $from, $to, $extra) = @_; + $from = 0 unless defined $from; + $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to); + + print "\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my %co = %{$commitlist->[$i]}; + if (!%co) { + next; + } + my $commit = $co{'id'}; + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + print "\n" . + format_author_html('td', \%co, 15, 5) . + "\n" . + "\n" . + "\n"; + } + if (defined $extra) { + print "\n" . + "\n" . + "\n"; + } + print "
$co{'age_string_date'}" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), + -class => "list subject"}, + chop_and_escape_str($co{'title'}, 50) . "
"); + my $comment = $co{'comment'}; + foreach my $line (@$comment) { + if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) { + my ($lead, $match, $trail) = ($1, $2, $3); + $match = chop_str($match, 70, 5, 'center'); + my $contextlen = int((80 - length($match))/2); + $contextlen = 30 if ($contextlen > 30); + $lead = chop_str($lead, $contextlen, 10, 'left'); + $trail = chop_str($trail, $contextlen, 10, 'right'); + + $lead = esc_html($lead); + $match = esc_html($match); + $trail = esc_html($trail); + + print "$lead$match$trail
"; + } + } + print "
" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . + " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree"); + print "
$extra
\n"; +} + +## ====================================================================== +## ====================================================================== +## actions + +sub git_project_list { + my $order = $input_params{'order'}; + if (defined $order && $order !~ m/none|project|descr|owner|age|language|license|version/) { + die_error(400, "Unknown order parameter"); + } + + my @list = git_get_projects_list($project_filter, $strict_export); + if (!@list) { + die_error(404, "No projects found"); + } + + git_header_html(); + if (defined $home_text && -f $home_text) { + print "
\n"; + insert_file($home_text); + print "
\n"; + } + + git_project_search_form($searchtext, $search_use_regexp); + git_project_list_body(\@list, $order); + git_footer_html(); +} + +sub git_forks { + my $order = $input_params{'order'}; + if (defined $order && $order !~ m/none|project|descr|owner|age|language|license|version/) { + die_error(400, "Unknown order parameter"); + } + + my $filter = $project; + $filter =~ s/\.git$//; + my @list = git_get_projects_list($filter); + if (!@list) { + die_error(404, "No forks found"); + } + + git_header_html(); + git_print_page_nav('',''); + git_print_header_div('summary', "$project forks"); + git_project_list_body(\@list, $order); + git_footer_html(); +} + +sub git_project_index { + my @projects = git_get_projects_list($project_filter, $strict_export); + if (!@projects) { + die_error(404, "No projects found"); + } + + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -content_disposition => 'inline; filename="index.aux"'); + + foreach my $pr (@projects) { + if (!exists $pr->{'owner'}) { + $pr->{'owner'} = git_get_project_owner("$pr->{'path'}"); + } + + my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'}); + # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' ' + $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg; + $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg; + $path =~ s/ /\+/g; + $owner =~ s/ /\+/g; + + print "$path $owner\n"; + } +} + +sub git_summary { + my $descr = git_get_project_description($project) || "none"; + my %co = parse_commit("HEAD"); + my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : (); + my $head = $co{'id'}; + my $remote_heads = gitweb_check_feature('remote_heads'); + + my $owner = git_get_project_owner($project); + + my $refs = git_get_references(); + # These get_*_list functions return one more to allow us to see if + # there are more ... + my @taglist = git_get_tags_list(16); + my @headlist = git_get_heads_list(16); + my %remotedata = $remote_heads ? git_get_remotes_list() : (); + my @forklist; + my $check_forks = gitweb_check_feature('forks'); + + if ($check_forks) { + # find forks of a project + my $filter = $project; + $filter =~ s/\.git$//; + @forklist = git_get_projects_list($filter); + # filter out forks of forks + @forklist = filter_forks_from_projects_list(\@forklist) + if (@forklist); + } + + git_header_html(); + git_print_page_nav('summary','', $head); + + print "
 
\n"; + print "\n" . + "\n"; + if ($owner and not $omit_owner) { + print "\n"; + } + if (defined $cd{'rfc2822'}) { + print "" . + "\n"; + } + + # use per project git URL list in $projectroot/$project/cloneurl + # or make project git URL from git base URL and project name + my $url_tag = "URL"; + my @url_list = git_get_project_url_list($project); + @url_list = map { "$_/$project" } @git_base_url_list unless @url_list; + foreach my $git_url (@url_list) { + next unless $git_url; + print format_repo_url($url_tag, $git_url); + $url_tag = ""; + } + + # Tag cloud + my $show_ctags = gitweb_check_feature('ctags'); + if ($show_ctags) { + my $ctags = git_get_project_ctags($project); + if (%$ctags) { + # without ability to add tags, don't show if there are none + my $cloud = git_populate_project_tagcloud($ctags); + print "" . + "" . + "" . + "\n"; + } + } + + print "
description" . esc_html($descr) . "
owner" . esc_html($owner) . "
last change".format_timestamp_html(\%cd)."
content tags".git_show_project_tagcloud($cloud, 48)."
\n"; + + # If XSS prevention is on, we don't include README.html. + # TODO: Allow a readme in some safe format. + if (!$prevent_xss && -s "$projectroot/$project/README.html") { + print "
readme
\n" . + "
\n"; + insert_file("$projectroot/$project/README.html"); + print "\n
\n"; # class="readme" + } + + # we need to request one more than 16 (0..15) to check if + # those 16 are all + my @commitlist = $head ? parse_commits($head, 17) : (); + if (@commitlist) { + git_print_header_div('shortlog'); + git_shortlog_body(\@commitlist, 0, 15, $refs, + $#commitlist <= 15 ? undef : + $cgi->a({-href => href(action=>"shortlog")}, "...")); + } + + if (@taglist) { + git_print_header_div('tags'); + git_tags_body(\@taglist, 0, 15, + $#taglist <= 15 ? undef : + $cgi->a({-href => href(action=>"tags")}, "...")); + } + + if (@headlist) { + git_print_header_div('heads'); + git_heads_body(\@headlist, $head, 0, 15, + $#headlist <= 15 ? undef : + $cgi->a({-href => href(action=>"heads")}, "...")); + } + + if (%remotedata) { + git_print_header_div('remotes'); + git_remotes_body(\%remotedata, 15, $head); + } + + if (@forklist) { + git_print_header_div('forks'); + git_project_list_body(\@forklist, 'age', 0, 15, + $#forklist <= 15 ? undef : + $cgi->a({-href => href(action=>"forks")}, "..."), + 'no_header'); + } + + git_footer_html(); +} + +sub git_tag { + my %tag = parse_tag($hash); + + if (! %tag) { + die_error(404, "Unknown tag object"); + } + + my $head = git_get_head_hash($project); + git_header_html(); + git_print_page_nav('','', $head,undef,$head); + git_print_header_div('commit', esc_html($tag{'name'}), $hash); + print "
\n" . + "\n" . + "\n" . + "\n" . + "\n" . + "\n" . + "\n"; + if (defined($tag{'author'})) { + git_print_authorship_rows(\%tag, 'author'); + } + print "
object" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})}, + $tag{'object'}) . "" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})}, + $tag{'type'}) . "
\n\n" . + "
\n"; + print "
"; + my $comment = $tag{'comment'}; + foreach my $line (@$comment) { + chomp $line; + print esc_html($line, -nbsp=>1) . "
\n"; + } + print "
\n"; + git_footer_html(); +} + +sub git_blame_common { + my $format = shift || 'porcelain'; + if ($format eq 'porcelain' && $input_params{'javascript'}) { + $format = 'incremental'; + $action = 'blame_incremental'; # for page title etc + } + + # permissions + gitweb_check_feature('blame') + or die_error(403, "Blame view not allowed"); + + # error checking + die_error(400, "No file name given") unless $file_name; + $hash_base ||= git_get_head_hash($project); + die_error(404, "Couldn't find base commit") unless $hash_base; + my %co = parse_commit($hash_base) + or die_error(404, "Commit not found"); + my $ftype = "blob"; + if (!defined $hash) { + $hash = git_get_hash_by_path($hash_base, $file_name, "blob") + or die_error(404, "Error looking up file"); + } else { + $ftype = git_get_type($hash); + if ($ftype !~ "blob") { + die_error(400, "Object is not a blob"); + } + } + + my $fd; + if ($format eq 'incremental') { + # get file contents (as base) + open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash + or die_error(500, "Open git-cat-file failed"); + } elsif ($format eq 'data') { + # run git-blame --incremental + open $fd, "-|", git_cmd(), "blame", "--incremental", + $hash_base, "--", $file_name + or die_error(500, "Open git-blame --incremental failed"); + } else { + # run git-blame --porcelain + open $fd, "-|", git_cmd(), "blame", '-p', + $hash_base, '--', $file_name + or die_error(500, "Open git-blame --porcelain failed"); + } + binmode $fd, ':utf8'; + + # incremental blame data returns early + if ($format eq 'data') { + print $cgi->header( + -type=>"text/plain", -charset => "utf-8", + -status=> "200 OK"); + local $| = 1; # output autoflush + while (my $line = <$fd>) { + print to_utf8($line); + } + close $fd + or print "ERROR $!\n"; + + print 'END'; + if (defined $t0 && gitweb_check_feature('timed')) { + print ' '. + tv_interval($t0, [ gettimeofday() ]). + ' '.$number_of_git_cmds; + } + print "\n"; + + return; + } + + # page header + git_header_html(); + my $formats_nav = + $cgi->a({-href => href(action=>"blob", -replay=>1)}, + "blob") . + " | "; + if ($format eq 'incremental') { + $formats_nav .= + $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)}, + "blame") . " (non-incremental)"; + } else { + $formats_nav .= + $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)}, + "blame") . " (incremental)"; + } + $formats_nav .= + " | " . + $cgi->a({-href => href(action=>"history", -replay=>1)}, + "history") . + " | " . + $cgi->a({-href => href(action=>$action, file_name=>$file_name)}, + "HEAD"); + git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + git_print_page_path($file_name, $ftype, $hash_base); + + # page body + if ($format eq 'incremental') { + print "\n"; + + print qq!
\n!; + } + + print qq!
\n!; + print qq!
... / ...
\n! + if ($format eq 'incremental'); + print qq!\n!. + #qq!\n!. + qq!\n!. + qq!\n!. + qq!\n!. + qq!\n!; + + my @rev_color = qw(light dark); + my $num_colors = scalar(@rev_color); + my $current_color = 0; + + if ($format eq 'incremental') { + my $color_class = $rev_color[$current_color]; + + #contents of a file + my $linenr = 0; + LINE: + while (my $line = <$fd>) { + chomp $line; + $linenr++; + + print qq!!. + qq!!. + qq!!; + print qq!\n"; + print qq!\n!; + } + + } else { # porcelain, i.e. ordinary blame + my %metainfo = (); # saves information about commits + + # blame data + LINE: + while (my $line = <$fd>) { + chomp $line; + # the header: [] + # no for subsequent lines in group of lines + my ($full_rev, $orig_lineno, $lineno, $group_size) = + ($line =~ /^($oid_regex) (\d+) (\d+)(?: (\d+))?$/); + if (!exists $metainfo{$full_rev}) { + $metainfo{$full_rev} = { 'nprevious' => 0 }; + } + my $meta = $metainfo{$full_rev}; + my $data; + while ($data = <$fd>) { + chomp $data; + last if ($data =~ s/^\t//); # contents of line + if ($data =~ /^(\S+)(?: (.*))?$/) { + $meta->{$1} = $2 unless exists $meta->{$1}; + } + if ($data =~ /^previous /) { + $meta->{'nprevious'}++; + } + } + my $short_rev = substr($full_rev, 0, 8); + my $author = $meta->{'author'}; + my %date = + parse_date($meta->{'author-time'}, $meta->{'author-tz'}); + my $date = $date{'iso-tz'}; + if ($group_size) { + $current_color = ($current_color + 1) % $num_colors; + } + my $tr_class = $rev_color[$current_color]; + $tr_class .= ' boundary' if (exists $meta->{'boundary'}); + $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0); + $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1); + print "\n"; + if ($group_size) { + print "\n"; + } + # 'previous' + if (exists $meta->{'previous'} && + $meta->{'previous'} =~ /^($oid_regex) (.*)$/) { + $meta->{'parent'} = $1; + $meta->{'file_parent'} = unquote($2); + } + my $linenr_commit = + exists($meta->{'parent'}) ? + $meta->{'parent'} : $full_rev; + my $linenr_filename = + exists($meta->{'file_parent'}) ? + $meta->{'file_parent'} : unquote($meta->{'filename'}); + my $blamed = href(action => 'blame', + file_name => $linenr_filename, + hash_base => $linenr_commit); + print ""; + print "\n"; + print "\n"; + } # end while + + } + + # footer + print "\n". + "
CommitLineData
!. + qq!$linenr! . esc_html($line) . "
1); + print ">"; + print $cgi->a({-href => href(action=>"commit", + hash=>$full_rev, + file_name=>$file_name)}, + esc_html($short_rev)); + if ($group_size >= 2) { + my @author_initials = ($author =~ /\b([[:upper:]])\B/g); + if (@author_initials) { + print "
" . + esc_html(join('', @author_initials)); + # or join('.', ...) + } + } + print "
"; + print $cgi->a({ -href => "$blamed#l$orig_lineno", + -class => "linenr" }, + esc_html($lineno)); + print "" . esc_html($data) . "
\n"; # class="blame" + print "
\n"; # class="blame_body" + close $fd + or print "Reading blob failed\n"; + + git_footer_html(); +} + +sub git_blame { + git_blame_common(); +} + +sub git_blame_incremental { + git_blame_common('incremental'); +} + +sub git_blame_data { + git_blame_common('data'); +} + +sub git_tags { + my $head = git_get_head_hash($project); + git_header_html(); + git_print_page_nav('','', $head,undef,$head,format_ref_views('tags')); + git_print_header_div('summary', $project); + + my @tagslist = git_get_tags_list(); + if (@tagslist) { + git_tags_body(\@tagslist); + } + git_footer_html(); +} + +sub git_heads { + my $head = git_get_head_hash($project); + git_header_html(); + git_print_page_nav('','', $head,undef,$head,format_ref_views('heads')); + git_print_header_div('summary', $project); + + my @headslist = git_get_heads_list(); + if (@headslist) { + git_heads_body(\@headslist, $head); + } + git_footer_html(); +} + +# used both for single remote view and for list of all the remotes +sub git_remotes { + gitweb_check_feature('remote_heads') + or die_error(403, "Remote heads view is disabled"); + + my $head = git_get_head_hash($project); + my $remote = $input_params{'hash'}; + + my $remotedata = git_get_remotes_list($remote); + die_error(500, "Unable to get remote information") unless defined $remotedata; + + unless (%$remotedata) { + die_error(404, defined $remote ? + "Remote $remote not found" : + "No remotes found"); + } + + git_header_html(undef, undef, -action_extra => $remote); + git_print_page_nav('', '', $head, undef, $head, + format_ref_views($remote ? '' : 'remotes')); + + fill_remote_heads($remotedata); + if (defined $remote) { + git_print_header_div('remotes', "$remote remote for $project"); + git_remote_block($remote, $remotedata->{$remote}, undef, $head); + } else { + git_print_header_div('summary', "$project remotes"); + git_remotes_body($remotedata, undef, $head); + } + + git_footer_html(); +} + +sub git_blob_plain { + my $type = shift; + my $expires; + + if (!defined $hash) { + if (defined $file_name) { + my $base = $hash_base || git_get_head_hash($project); + $hash = git_get_hash_by_path($base, $file_name, "blob") + or die_error(404, "Cannot find file"); + } else { + die_error(400, "No file name defined"); + } + } elsif ($hash =~ m/^$oid_regex$/) { + # blobs defined by non-textual hash id's can be cached + $expires = "+1d"; + } + + open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash + or die_error(500, "Open git-cat-file blob '$hash' failed"); + + # content-type (can include charset) + $type = blob_contenttype($fd, $file_name, $type); + + # "save as" filename, even when no $file_name is given + my $save_as = "$hash"; + if (defined $file_name) { + $save_as = $file_name; + } elsif ($type =~ m/^text\//) { + $save_as .= '.txt'; + } + + # With XSS prevention on, blobs of all types except a few known safe + # ones are served with "Content-Disposition: attachment" to make sure + # they don't run in our security domain. For certain image types, + # blob view writes an tag referring to blob_plain view, and we + # want to be sure not to break that by serving the image as an + # attachment (though Firefox 3 doesn't seem to care). + my $sandbox = $prevent_xss && + $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!; + + # serve text/* as text/plain + if ($prevent_xss && + ($type =~ m!^text/[a-z]+\b(.*)$! || + ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) { + my $rest = $1; + $rest = defined $rest ? $rest : ''; + $type = "text/plain$rest"; + } + + print $cgi->header( + -type => $type, + -expires => $expires, + -content_disposition => + ($sandbox ? 'attachment' : 'inline') + . '; filename="' . $save_as . '"'); + local $/ = undef; + local *FCGI::Stream::PRINT = $FCGI_Stream_PRINT_raw; + binmode STDOUT, ':raw'; + print <$fd>; + binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi + close $fd; +} + +sub git_blob { + my $expires; + + if (!defined $hash) { + if (defined $file_name) { + my $base = $hash_base || git_get_head_hash($project); + $hash = git_get_hash_by_path($base, $file_name, "blob") + or die_error(404, "Cannot find file"); + } else { + die_error(400, "No file name defined"); + } + } elsif ($hash =~ m/^$oid_regex$/) { + # blobs defined by non-textual hash id's can be cached + $expires = "+1d"; + } + + my $have_blame = gitweb_check_feature('blame'); + open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash + or die_error(500, "Couldn't cat $file_name, $hash"); + my $mimetype = blob_mimetype($fd, $file_name); + # use 'blob_plain' (aka 'raw') view for files that cannot be displayed + if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) { + close $fd; + return git_blob_plain($mimetype); + } + # we can have blame only for text/* mimetype + $have_blame &&= ($mimetype =~ m!^text/!); + + my $highlight = gitweb_check_feature('highlight'); + my $syntax = guess_file_syntax($highlight, $file_name); + $fd = run_highlighter($fd, $highlight, $syntax); + + git_header_html(undef, $expires); + my $formats_nav = ''; + if (defined $hash_base && (my %co = parse_commit($hash_base))) { + if (defined $file_name) { + if ($have_blame) { + $formats_nav .= + $cgi->a({-href => href(action=>"blame", -replay=>1)}, + "blame") . + " | "; + } + $formats_nav .= + $cgi->a({-href => href(action=>"history", -replay=>1)}, + "history") . + " | " . + $cgi->a({-href => href(action=>"blob_plain", -replay=>1)}, + "raw") . + " | " . + $cgi->a({-href => href(action=>"blob", + hash_base=>"HEAD", file_name=>$file_name)}, + "HEAD"); + } else { + $formats_nav .= + $cgi->a({-href => href(action=>"blob_plain", -replay=>1)}, + "raw"); + } + git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + } else { + print "
\n" . + "

\n" . + "
".esc_html($hash)."
\n"; + } + git_print_page_path($file_name, "blob", $hash_base); + print "
\n"; + if ($mimetype =~ m!^image/!) { + print qq!!.esc_attr($file_name).qq!$hash, + hash_base=>$hash_base, file_name=>$file_name)) . + qq!" />\n!; + } else { + my $nr; + while (my $line = <$fd>) { + chomp $line; + $nr++; + $line = untabify($line); + printf qq!
%4i %s
\n!, + $nr, esc_attr(href(-replay => 1)), $nr, $nr, + $highlight ? sanitize($line) : esc_html($line, -nbsp=>1); + } + } + close $fd + or print "Reading blob failed.\n"; + print "
"; + git_footer_html(); +} + +sub git_tree { + if (!defined $hash_base) { + $hash_base = "HEAD"; + } + if (!defined $hash) { + if (defined $file_name) { + $hash = git_get_hash_by_path($hash_base, $file_name, "tree"); + } else { + $hash = $hash_base; + } + } + die_error(404, "No such tree") unless defined($hash); + + my $show_sizes = gitweb_check_feature('show-sizes'); + my $have_blame = gitweb_check_feature('blame'); + + my @entries = (); + { + local $/ = "\0"; + open my $fd, "-|", git_cmd(), "ls-tree", '-z', + ($show_sizes ? '-l' : ()), @extra_options, $hash + or die_error(500, "Open git-ls-tree failed"); + @entries = map { chomp; $_ } <$fd>; + close $fd + or die_error(404, "Reading tree failed"); + } + + # Sort entries: directories first, then files, alphabetically within each group + @entries = sort { + my %a_parsed = parse_ls_tree_line($a, -z => 1, -l => $show_sizes); + my %b_parsed = parse_ls_tree_line($b, -z => 1, -l => $show_sizes); + + # Compare types: tree (directory) comes before blob (file) + my $type_cmp = ($b_parsed{'type'} eq 'tree') <=> ($a_parsed{'type'} eq 'tree'); + return $type_cmp if $type_cmp != 0; + + # If same type, sort alphabetically by name + return lc($a_parsed{'name'}) cmp lc($b_parsed{'name'}); + } @entries; + + my $refs = git_get_references(); + my $ref = format_ref_marker($refs, $hash_base); + git_header_html(); + my $basedir = ''; + if (defined $hash_base && (my %co = parse_commit($hash_base))) { + my @views_nav = (); + if (defined $file_name) { + push @views_nav, + $cgi->a({-href => href(action=>"history", -replay=>1)}, + "history"), + $cgi->a({-href => href(action=>"tree", + hash_base=>"HEAD", file_name=>$file_name)}, + "HEAD"), + } + my $snapshot_links = format_snapshot_links($hash); + if (defined $snapshot_links) { + # FIXME: Should be available when we have no hash base as well. + push @views_nav, $snapshot_links; + } + git_print_page_nav('tree','', $hash_base, undef, undef, + join(' | ', @views_nav)); + git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base); + } else { + undef $hash_base; + print "
\n"; + print "

\n"; + print "
".esc_html($hash)."
\n"; + } + if (defined $file_name) { + $basedir = $file_name; + if ($basedir ne '' && substr($basedir, -1) ne '/') { + $basedir .= '/'; + } + git_print_page_path($file_name, 'tree', $hash_base); + } + print "
\n"; + print "\n"; + my $alternate = 1; + # '..' (top directory) link if possible + if (defined $hash_base && + defined $file_name && $file_name =~ m![^/]+$!) { + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + + my $up = $file_name; + $up =~ s!/?[^/]+$!!; + undef $up unless $up; + # based on git_print_tree_entry + print '\n"; + print ''."\n" if $show_sizes; + print '\n"; + print "\n"; + + print "\n"; + } + foreach my $line (@entries) { + my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes); + + if ($alternate) { + print "\n"; + } else { + print "\n"; + } + $alternate ^= 1; + + git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame); + + print "\n"; + } + print "
' . mode_str('040000') . " '; + print $cgi->a({-href => href(action=>"tree", + hash_base=>$hash_base, + file_name=>$up)}, + ".."); + print "
\n" . + "
"; + git_footer_html(); +} + +sub sanitize_for_filename { + my $name = shift; + + $name =~ s!/!-!g; + $name =~ s/[^[:alnum:]_.-]//g; + + return $name; +} + +sub snapshot_name { + my ($project, $hash) = @_; + + # path/to/project.git -> project + # path/to/project/.git -> project + my $name = to_utf8($project); + $name =~ s,([^/])/*\.git$,$1,; + $name = sanitize_for_filename(basename($name)); + + my $ver = $hash; + if ($hash =~ /^[0-9a-fA-F]+$/) { + # shorten SHA-1 hash + my $full_hash = git_get_full_hash($project, $hash); + if ($full_hash =~ /^$hash/ && length($hash) > 7) { + $ver = git_get_short_hash($project, $hash); + } + } elsif ($hash =~ m!^refs/tags/(.*)$!) { + # tags don't need shortened SHA-1 hash + $ver = $1; + } else { + # branches and other need shortened SHA-1 hash + my $strip_refs = join '|', map { quotemeta } get_branch_refs(); + if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) { + my $ref_dir = (defined $1) ? $1 : ''; + $ver = $2; + + $ref_dir = sanitize_for_filename($ref_dir); + # for refs neither in heads nor remotes we want to + # add a ref dir to archive name + if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') { + $ver = $ref_dir . '-' . $ver; + } + } + $ver .= '-' . git_get_short_hash($project, $hash); + } + # special case of sanitization for filename - we change + # slashes to dots instead of dashes + # in case of hierarchical branch names + $ver =~ s!/!.!g; + $ver =~ s/[^[:alnum:]_.-]//g; + + # name = project-version_string + $name = "$name-$ver"; + + return wantarray ? ($name, $name) : $name; +} + +sub exit_if_unmodified_since { + my ($latest_epoch) = @_; + our $cgi; + + my $if_modified = $cgi->http('IF_MODIFIED_SINCE'); + if (defined $if_modified) { + my $since; + if (eval { require HTTP::Date; 1; }) { + $since = HTTP::Date::str2time($if_modified); + } elsif (eval { require Time::ParseDate; 1; }) { + $since = Time::ParseDate::parsedate($if_modified, GMT => 1); + } + if (defined $since && $latest_epoch <= $since) { + my %latest_date = parse_date($latest_epoch); + print $cgi->header( + -last_modified => $latest_date{'rfc2822'}, + -status => '304 Not Modified'); + goto DONE_GITWEB; + } + } +} + +sub git_snapshot { + my $format = $input_params{'snapshot_format'}; + if (!@snapshot_fmts) { + die_error(403, "Snapshots not allowed"); + } + # default to first supported snapshot format + $format ||= $snapshot_fmts[0]; + if ($format !~ m/^[a-z0-9]+$/) { + die_error(400, "Invalid snapshot format parameter"); + } elsif (!exists($known_snapshot_formats{$format})) { + die_error(400, "Unknown snapshot format"); + } elsif ($known_snapshot_formats{$format}{'disabled'}) { + die_error(403, "Snapshot format not allowed"); + } elsif (!grep($_ eq $format, @snapshot_fmts)) { + die_error(403, "Unsupported snapshot format"); + } + + my $type = git_get_type("$hash^{}"); + if (!$type) { + die_error(404, 'Object does not exist'); + } elsif ($type eq 'blob') { + die_error(400, 'Object is not a tree-ish'); + } + + my ($name, $prefix) = snapshot_name($project, $hash); + my $filename = "$name$known_snapshot_formats{$format}{'suffix'}"; + + my %co = parse_commit($hash); + exit_if_unmodified_since($co{'committer_epoch'}) if %co; + + my $cmd = quote_command( + git_cmd(), 'archive', + "--format=$known_snapshot_formats{$format}{'format'}", + "--prefix=$prefix/", $hash); + if (exists $known_snapshot_formats{$format}{'compressor'}) { + $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}}); + } + + $filename =~ s/(["\\])/\\$1/g; + my %latest_date; + if (%co) { + %latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'}); + } + + print $cgi->header( + -type => $known_snapshot_formats{$format}{'type'}, + -content_disposition => 'inline; filename="' . $filename . '"', + %co ? (-last_modified => $latest_date{'rfc2822'}) : (), + -status => '200 OK'); + + open my $fd, "-|", $cmd + or die_error(500, "Execute git-archive failed"); + local *FCGI::Stream::PRINT = $FCGI_Stream_PRINT_raw; + binmode STDOUT, ':raw'; + print <$fd>; + binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi + close $fd; +} + +sub git_log_generic { + my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_; + + my $head = git_get_head_hash($project); + if (!defined $base) { + $base = $head; + } + if (!defined $page) { + $page = 0; + } + my $refs = git_get_references(); + + my $commit_hash = $base; + if (defined $parent) { + $commit_hash = "$parent..$base"; + } + my @commitlist = + parse_commits($commit_hash, 101, (100 * $page), + defined $file_name ? ($file_name, "--full-history") : ()); + + my $ftype; + if (!defined $file_hash && defined $file_name) { + # some commits could have deleted file in question, + # and not have it in tree, but one of them has to have it + for (my $i = 0; $i < @commitlist; $i++) { + $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name); + last if defined $file_hash; + } + } + if (defined $file_hash) { + $ftype = git_get_type($file_hash); + } + if (defined $file_name && !defined $ftype) { + die_error(500, "Unknown type of object"); + } + my %co; + if (defined $file_name) { + %co = parse_commit($base) + or die_error(404, "Unknown commit object"); + } + + + my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100); + my $next_link = ''; + if ($#commitlist >= 100) { + $next_link = + $cgi->a({-href => href(-replay=>1, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } + my $patch_max = gitweb_get_feature('patches'); + if ($patch_max && !defined $file_name && + !gitweb_check_feature('email-privacy')) { + if ($patch_max < 0 || @commitlist <= $patch_max) { + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"patches", -replay=>1)}, + "patches"); + } + } + + git_header_html(); + git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav); + if (defined $file_name) { + git_print_header_div('commit', esc_html($co{'title'}), $base); + } else { + git_print_header_div('summary', $project) + } + git_print_page_path($file_name, $ftype, $hash_base) + if (defined $file_name); + + $body_subr->(\@commitlist, 0, 99, $refs, $next_link, + $file_name, $file_hash, $ftype); + + git_footer_html(); +} + +sub git_log { + git_log_generic('log', \&git_log_body, + $hash, $hash_parent); +} + +sub git_commit { + $hash ||= $hash_base || "HEAD"; + my %co = parse_commit($hash) + or die_error(404, "Unknown commit object"); + + my $parent = $co{'parent'}; + my $parents = $co{'parents'}; # listref + + # we need to prepare $formats_nav before any parameter munging + my $formats_nav; + if (!defined $parent) { + # --root commitdiff + $formats_nav .= '(initial)'; + } elsif (@$parents == 1) { + # single parent commit + $formats_nav .= + '(parent: ' . + $cgi->a({-href => href(action=>"commit", + hash=>$parent)}, + esc_html(substr($parent, 0, 7))) . + ')'; + } else { + # merge commit + $formats_nav .= + '(merge: ' . + join(' ', map { + $cgi->a({-href => href(action=>"commit", + hash=>$_)}, + esc_html(substr($_, 0, 7))); + } @$parents ) . + ')'; + } + if (gitweb_check_feature('patches') && @$parents <= 1 && + !gitweb_check_feature('email-privacy')) { + $formats_nav .= " | " . + $cgi->a({-href => href(action=>"patch", -replay=>1)}, + "patch"); + } + + if (!defined $parent) { + $parent = "--root"; + } + my @difftree; + open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id", + @diff_opts, + (@$parents <= 1 ? $parent : '-c'), + $hash, "--" + or die_error(500, "Open git-diff-tree failed"); + @difftree = map { chomp; $_ } <$fd>; + close $fd or die_error(404, "Reading git-diff-tree failed"); + + # non-textual hash id's can be cached + my $expires; + if ($hash =~ m/^$oid_regex$/) { + $expires = "+1d"; + } + my $refs = git_get_references(); + my $ref = format_ref_marker($refs, $co{'id'}); + + git_header_html(undef, $expires); + git_print_page_nav('commit', '', + $hash, $co{'tree'}, $hash, + $formats_nav); + + if (defined $co{'parent'}) { + git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash); + } else { + git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash); + } + print "
\n" . + "\n"; + git_print_authorship_rows(\%co); + print "\n"; + print "" . + "" . + "" . + "" . + "\n"; + + foreach my $par (@$parents) { + print "" . + "" . + "" . + "" . + "\n"; + } + print "
commit$co{'id'}
tree" . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash), + class => "list"}, $co{'tree'}) . + "" . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)}, + "tree"); + my $snapshot_links = format_snapshot_links($hash); + if (defined $snapshot_links) { + print " | " . $snapshot_links; + } + print "
parent" . + $cgi->a({-href => href(action=>"commit", hash=>$par), + class => "list"}, $par) . + "" . + $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") . + " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") . + "
". + "
\n"; + + print "
\n"; + git_print_log($co{'comment'}); + print "
\n"; + + git_difftree_body(\@difftree, $hash, @$parents); + + git_footer_html(); +} + +sub git_object { + # object is defined by: + # - hash or hash_base alone + # - hash_base and file_name + my $type; + + # - hash or hash_base alone + if ($hash || ($hash_base && !defined $file_name)) { + my $object_id = $hash || $hash_base; + + open my $fd, "-|", quote_command( + git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null' + or die_error(404, "Object does not exist"); + $type = <$fd>; + defined $type && chomp $type; + close $fd + or die_error(404, "Object does not exist"); + + # - hash_base and file_name + } elsif ($hash_base && defined $file_name) { + $file_name =~ s,/+$,,; + + system(git_cmd(), "cat-file", '-e', $hash_base) == 0 + or die_error(404, "Base object does not exist"); + + # here errors should not happen + open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name + or die_error(500, "Open git-ls-tree failed"); + my $line = <$fd>; + close $fd; + + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' + unless ($line && $line =~ m/^([0-9]+) (.+) ($oid_regex)\t/) { + die_error(404, "File or directory for given base does not exist"); + } + $type = $2; + $hash = $3; + } else { + die_error(400, "Not enough information to find object"); + } + + print $cgi->redirect(-uri => href(action=>$type, -full=>1, + hash=>$hash, hash_base=>$hash_base, + file_name=>$file_name), + -status => '302 Found'); +} + +sub git_blobdiff { + my $format = shift || 'html'; + my $diff_style = $input_params{'diff_style'} || 'inline'; + + my $fd; + my @difftree; + my %diffinfo; + my $expires; + + # preparing $fd and %diffinfo for git_patchset_body + # new style URI + if (defined $hash_base && defined $hash_parent_base) { + if (defined $file_name) { + # read raw output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + $hash_parent_base, $hash_base, + "--", (defined $file_parent ? $file_parent : ()), $file_name + or die_error(500, "Open git-diff-tree failed"); + @difftree = map { chomp; $_ } <$fd>; + close $fd + or die_error(404, "Reading git-diff-tree failed"); + @difftree + or die_error(404, "Blob diff not found"); + + } elsif (defined $hash && + $hash =~ $oid_regex) { + # try to find filename from $hash + + # read filtered raw output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + $hash_parent_base, $hash_base, "--" + or die_error(500, "Open git-diff-tree failed"); + @difftree = + # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c' + # $hash == to_id + grep { /^:[0-7]{6} [0-7]{6} $oid_regex $hash/ } + map { chomp; $_ } <$fd>; + close $fd + or die_error(404, "Reading git-diff-tree failed"); + @difftree + or die_error(404, "Blob diff not found"); + + } else { + die_error(400, "Missing one of the blob diff parameters"); + } + + if (@difftree > 1) { + die_error(400, "Ambiguous blob diff specification"); + } + + %diffinfo = parse_difftree_raw_line($difftree[0]); + $file_parent ||= $diffinfo{'from_file'} || $file_name; + $file_name ||= $diffinfo{'to_file'}; + + $hash_parent ||= $diffinfo{'from_id'}; + $hash ||= $diffinfo{'to_id'}; + + # non-textual hash id's can be cached + if ($hash_base =~ m/^$oid_regex$/ && + $hash_parent_base =~ m/^$oid_regex$/) { + $expires = '+1d'; + } + + # open patch output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + '-p', ($format eq 'html' ? "--full-index" : ()), + $hash_parent_base, $hash_base, + "--", (defined $file_parent ? $file_parent : ()), $file_name + or die_error(500, "Open git-diff-tree failed"); + } + + # old/legacy style URI -- not generated anymore since 1.4.3. + if (!%diffinfo) { + die_error('404 Not Found', "Missing one of the blob diff parameters") + } + + # header + if ($format eq 'html') { + my $formats_nav = + $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)}, + "raw"); + $formats_nav .= diff_style_nav($diff_style); + git_header_html(undef, $expires); + if (defined $hash_base && (my %co = parse_commit($hash_base))) { + git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + } else { + print "

$formats_nav
\n"; + print "
".esc_html("$hash vs $hash_parent")."
\n"; + } + if (defined $file_name) { + git_print_page_path($file_name, "blob", $hash_base); + } else { + print "
\n"; + } + + } elsif ($format eq 'plain') { + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -expires => $expires, + -content_disposition => 'inline; filename="' . "$file_name" . '.patch"'); + + print "X-Git-Url: " . $cgi->self_url() . "\n\n"; + + } else { + die_error(400, "Unknown blobdiff format"); + } + + # patch + if ($format eq 'html') { + print "
\n"; + + git_patchset_body($fd, $diff_style, + [ \%diffinfo ], $hash_base, $hash_parent_base); + close $fd; + + print "
\n"; # class="page_body" + git_footer_html(); + + } else { + while (my $line = <$fd>) { + $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg; + $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg; + + print $line; + + last if $line =~ m!^\+\+\+!; + } + local $/ = undef; + print <$fd>; + close $fd; + } +} + +sub git_blobdiff_plain { + git_blobdiff('plain'); +} + +# assumes that it is added as later part of already existing navigation, +# so it returns "| foo | bar" rather than just "foo | bar" +sub diff_style_nav { + my ($diff_style, $is_combined) = @_; + $diff_style ||= 'inline'; + + return "" if ($is_combined); + + my @styles = (inline => 'inline', 'sidebyside' => 'side by side'); + my %styles = @styles; + @styles = + @styles[ map { $_ * 2 } 0..$#styles/2 ]; + + return join '', + map { " | ".$_ } + map { + $_ eq $diff_style ? $styles{$_} : + $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_}) + } @styles; +} + +sub git_commitdiff { + my %params = @_; + my $format = $params{-format} || 'html'; + my $diff_style = $input_params{'diff_style'} || 'inline'; + + my ($patch_max) = gitweb_get_feature('patches'); + if ($format eq 'patch') { + die_error(403, "Patch view not allowed") unless $patch_max; + } + + $hash ||= $hash_base || "HEAD"; + my %co = parse_commit($hash) + or die_error(404, "Unknown commit object"); + + # choose format for commitdiff for merge + if (! defined $hash_parent && @{$co{'parents'}} > 1) { + $hash_parent = '--cc'; + } + # we need to prepare $formats_nav before almost any parameter munging + my $formats_nav; + if ($format eq 'html') { + $formats_nav = + $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)}, + "raw"); + if ($patch_max && @{$co{'parents'}} <= 1 && + !gitweb_check_feature('email-privacy')) { + $formats_nav .= " | " . + $cgi->a({-href => href(action=>"patch", -replay=>1)}, + "patch"); + } + $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1); + + if (defined $hash_parent && + $hash_parent ne '-c' && $hash_parent ne '--cc') { + # commitdiff with two commits given + my $hash_parent_short = $hash_parent; + if ($hash_parent =~ m/^$oid_regex$/) { + $hash_parent_short = substr($hash_parent, 0, 7); + } + $formats_nav .= + ' (from'; + for (my $i = 0; $i < @{$co{'parents'}}; $i++) { + if ($co{'parents'}[$i] eq $hash_parent) { + $formats_nav .= ' parent ' . ($i+1); + last; + } + } + $formats_nav .= ': ' . + $cgi->a({-href => href(-replay=>1, + hash=>$hash_parent, hash_base=>undef)}, + esc_html($hash_parent_short)) . + ')'; + } elsif (!$co{'parent'}) { + # --root commitdiff + $formats_nav .= ' (initial)'; + } elsif (scalar @{$co{'parents'}} == 1) { + # single parent commit + $formats_nav .= + ' (parent: ' . + $cgi->a({-href => href(-replay=>1, + hash=>$co{'parent'}, hash_base=>undef)}, + esc_html(substr($co{'parent'}, 0, 7))) . + ')'; + } else { + # merge commit + if ($hash_parent eq '--cc') { + $formats_nav .= ' | ' . + $cgi->a({-href => href(-replay=>1, + hash=>$hash, hash_parent=>'-c')}, + 'combined'); + } else { # $hash_parent eq '-c' + $formats_nav .= ' | ' . + $cgi->a({-href => href(-replay=>1, + hash=>$hash, hash_parent=>'--cc')}, + 'compact'); + } + $formats_nav .= + ' (merge: ' . + join(' ', map { + $cgi->a({-href => href(-replay=>1, + hash=>$_, hash_base=>undef)}, + esc_html(substr($_, 0, 7))); + } @{$co{'parents'}} ) . + ')'; + } + } + + my $hash_parent_param = $hash_parent; + if (!defined $hash_parent_param) { + # --cc for multiple parents, --root for parentless + $hash_parent_param = + @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root'; + } + + # read commitdiff + my $fd; + my @difftree; + if ($format eq 'html') { + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + "--no-commit-id", "--patch-with-raw", "--full-index", + $hash_parent_param, $hash, "--" + or die_error(500, "Open git-diff-tree failed"); + + while (my $line = <$fd>) { + chomp $line; + # empty line ends raw part of diff-tree output + last unless $line; + push @difftree, scalar parse_difftree_raw_line($line); + } + + } elsif ($format eq 'plain') { + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + '-p', $hash_parent_param, $hash, "--" + or die_error(500, "Open git-diff-tree failed"); + } elsif ($format eq 'patch') { + # For commit ranges, we limit the output to the number of + # patches specified in the 'patches' feature. + # For single commits, we limit the output to a single patch, + # diverging from the git-format-patch default. + my @commit_spec = (); + if ($hash_parent) { + if ($patch_max > 0) { + push @commit_spec, "-$patch_max"; + } + push @commit_spec, '-n', "$hash_parent..$hash"; + } else { + if ($params{-single}) { + push @commit_spec, '-1'; + } else { + if ($patch_max > 0) { + push @commit_spec, "-$patch_max"; + } + push @commit_spec, "-n"; + } + push @commit_spec, '--root', $hash; + } + open $fd, "-|", git_cmd(), "format-patch", @diff_opts, + '--encoding=utf8', '--stdout', @commit_spec + or die_error(500, "Open git-format-patch failed"); + } else { + die_error(400, "Unknown commitdiff format"); + } + + # non-textual hash id's can be cached + my $expires; + if ($hash =~ m/^$oid_regex$/) { + $expires = "+1d"; + } + + # write commit message + if ($format eq 'html') { + my $refs = git_get_references(); + my $ref = format_ref_marker($refs, $co{'id'}); + + git_header_html(undef, $expires); + git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash); + print "
\n" . + "\n"; + git_print_authorship_rows(\%co); + print "
". + "
\n"; + print "
\n"; + if (@{$co{'comment'}} > 1) { + print "
\n"; + git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1); + print "
\n"; # class="log" + } + + } elsif ($format eq 'plain') { + my $refs = git_get_references("tags"); + my $tagname = git_get_rev_name_tags($hash); + my $filename = basename($project) . "-$hash.patch"; + + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -expires => $expires, + -content_disposition => 'inline; filename="' . "$filename" . '"'); + my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'}); + print "From: " . to_utf8($co{'author'}) . "\n"; + print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n"; + print "Subject: " . to_utf8($co{'title'}) . "\n"; + + print "X-Git-Tag: $tagname\n" if $tagname; + print "X-Git-Url: " . $cgi->self_url() . "\n\n"; + + foreach my $line (@{$co{'comment'}}) { + print to_utf8($line) . "\n"; + } + print "---\n\n"; + } elsif ($format eq 'patch') { + my $filename = basename($project) . "-$hash.patch"; + + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -expires => $expires, + -content_disposition => 'inline; filename="' . "$filename" . '"'); + } + + # write patch + if ($format eq 'html') { + my $use_parents = !defined $hash_parent || + $hash_parent eq '-c' || $hash_parent eq '--cc'; + git_difftree_body(\@difftree, $hash, + $use_parents ? @{$co{'parents'}} : $hash_parent); + print "
\n"; + + git_patchset_body($fd, $diff_style, + \@difftree, $hash, + $use_parents ? @{$co{'parents'}} : $hash_parent); + close $fd; + print "
\n"; # class="page_body" + git_footer_html(); + + } elsif ($format eq 'plain') { + local $/ = undef; + print <$fd>; + close $fd + or print "Reading git-diff-tree failed\n"; + } elsif ($format eq 'patch') { + local $/ = undef; + print <$fd>; + close $fd + or print "Reading git-format-patch failed\n"; + } +} + +sub git_commitdiff_plain { + git_commitdiff(-format => 'plain'); +} + +# format-patch-style patches +sub git_patch { + git_commitdiff(-format => 'patch', -single => 1); +} + +sub git_patches { + git_commitdiff(-format => 'patch'); +} + +sub git_history { + git_log_generic('history', \&git_history_body, + $hash_base, $hash_parent_base, + $file_name, $hash); +} + +sub git_search { + $searchtype ||= 'commit'; + + # check if appropriate features are enabled + gitweb_check_feature('search') + or die_error(403, "Search is disabled"); + if ($searchtype eq 'pickaxe') { + # pickaxe may take all resources of your box and run for several minutes + # with every query - so decide by yourself how public you make this feature + gitweb_check_feature('pickaxe') + or die_error(403, "Pickaxe search is disabled"); + } + if ($searchtype eq 'grep') { + # grep search might be potentially CPU-intensive, too + gitweb_check_feature('grep') + or die_error(403, "Grep search is disabled"); + } + + if (!defined $searchtext) { + die_error(400, "Text field is empty"); + } + if (!defined $hash) { + $hash = git_get_head_hash($project); + } + my %co = parse_commit($hash); + if (!%co) { + die_error(404, "Unknown commit object"); + } + if (!defined $page) { + $page = 0; + } + + if ($searchtype eq 'commit' || + $searchtype eq 'author' || + $searchtype eq 'committer') { + git_search_message(%co); + } elsif ($searchtype eq 'pickaxe') { + git_search_changes(%co); + } elsif ($searchtype eq 'grep') { + git_search_files(%co); + } else { + die_error(400, "Unknown search type"); + } +} + +sub git_search_help { + git_header_html(); + git_print_page_nav('','', $hash,$hash,$hash); + print <Pattern is by default a normal string that is matched precisely (but without +regard to case, except in the case of pickaxe). However, when you check the re checkbox, +the pattern entered is recognized as the POSIX extended +regular expression (also case +insensitive).

+
+
commit
+
The commit messages and authorship information will be scanned for the given pattern.
+EOT + my $have_grep = gitweb_check_feature('grep'); + if ($have_grep) { + print <grep +
All files in the currently selected tree (HEAD unless you are explicitly browsing + a different one) are searched for the given pattern. On large trees, this search can take +a while and put some strain on the server, so please use it with some consideration. Note that +due to git-grep peculiarity, currently if regexp mode is turned off, the matches are +case-sensitive.
+EOT + } + print <author +
Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.
+
committer
+
Name and e-mail of the committer and date of commit will be scanned for the given pattern.
+EOT + my $have_pickaxe = gitweb_check_feature('pickaxe'); + if ($have_pickaxe) { + print <pickaxe +
All commits that caused the string to appear or disappear from any file (changes that +added, removed or "modified" the string) will be listed. This search can take a while and +takes a lot of strain on the server, so please use it wisely. Note that since you may be +interested even in changes just changing the case as well, this search is case sensitive.
+EOT + } + print "
\n"; + git_footer_html(); +} + +sub git_shortlog { + git_log_generic('shortlog', \&git_shortlog_body, + $hash, $hash_parent); +} + +## ...................................................................... +## feeds (RSS, Atom; OPML) + +sub git_feed { + my $format = shift || 'atom'; + my $have_blame = gitweb_check_feature('blame'); + + # Atom: http://www.atomenabled.org/developers/syndication/ + # RSS: https://web.archive.org/web/20030729001534/http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ + if ($format ne 'rss' && $format ne 'atom') { + die_error(400, "Unknown web feed format"); + } + + # log/feed of current (HEAD) branch, log of given branch, history of file/directory + my $head = $hash || 'HEAD'; + my @commitlist = parse_commits($head, 150, 0, $file_name); + + my %latest_commit; + my %latest_date; + my $content_type = "application/$format+xml"; + if (defined $cgi->http('HTTP_ACCEPT') && + $cgi->Accept('text/xml') > $cgi->Accept($content_type)) { + # browser (feed reader) prefers text/xml + $content_type = 'text/xml'; + } + if (defined($commitlist[0])) { + %latest_commit = %{$commitlist[0]}; + my $latest_epoch = $latest_commit{'committer_epoch'}; + exit_if_unmodified_since($latest_epoch); + %latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'}); + } + print $cgi->header( + -type => $content_type, + -charset => 'utf-8', + %latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (), + -status => '200 OK'); + + # Optimization: skip generating the body if client asks only + # for Last-Modified date. + return if ($cgi->request_method() eq 'HEAD'); + + # header variables + my $title = "$site_name - $project/$action"; + my $feed_type = 'log'; + if (defined $hash) { + $title .= " - '$hash'"; + $feed_type = 'branch log'; + if (defined $file_name) { + $title .= " :: $file_name"; + $feed_type = 'history'; + } + } elsif (defined $file_name) { + $title .= " - $file_name"; + $feed_type = 'history'; + } + $title .= " $feed_type"; + $title = esc_html($title); + my $descr = git_get_project_description($project); + if (defined $descr) { + $descr = esc_html($descr); + } else { + $descr = "$project " . + ($format eq 'rss' ? 'RSS' : 'Atom') . + " feed"; + } + my $owner = git_get_project_owner($project); + $owner = esc_html($owner); + + #header + my $alt_url; + if (defined $file_name) { + $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name); + } elsif (defined $hash) { + $alt_url = href(-full=>1, action=>"log", hash=>$hash); + } else { + $alt_url = href(-full=>1, action=>"summary"); + } + $alt_url = esc_attr($alt_url); + print qq!\n!; + if ($format eq 'rss') { + print < + +XML + print "$title\n" . + "$alt_url\n" . + "$descr\n" . + "en\n" . + # project owner is responsible for 'editorial' content + "$owner\n"; + if (defined $logo || defined $favicon) { + # prefer the logo to the favicon, since RSS + # doesn't allow both + my $img = esc_url($logo || $favicon); + print "\n" . + "$img\n" . + "$title\n" . + "$alt_url\n" . + "\n"; + } + if (%latest_date) { + print "$latest_date{'rfc2822'}\n"; + print "$latest_date{'rfc2822'}\n"; + } + print "gitweb v.$version/$git_version\n"; + } elsif ($format eq 'atom') { + print < +XML + print "$title\n" . + "$descr\n" . + '' . "\n" . + '' . "\n" . + "" . esc_url(href(-full=>1)) . "\n" . + # use project owner for feed author + "$owner\n"; + if (defined $favicon) { + print "" . esc_url($favicon) . "\n"; + } + if (defined $logo) { + # not twice as wide as tall: 72 x 27 pixels + print "" . esc_url($logo) . "\n"; + } + if (! %latest_date) { + # dummy date to keep the feed valid until commits trickle in: + print "1970-01-01T00:00:00Z\n"; + } else { + print "$latest_date{'iso-8601'}\n"; + } + print "gitweb\n"; + } + + # contents + for (my $i = 0; $i <= $#commitlist; $i++) { + my %co = %{$commitlist[$i]}; + my $commit = $co{'id'}; + # we read 150, we always show 30 and the ones more recent than 48 hours + if (($i >= 20) && ((time - $co{'committer_epoch'}) > 48*60*60)) { + last; + } + my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'}); + + # get list of changed files + open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + $co{'parent'} || "--root", + $co{'id'}, "--", (defined $file_name ? $file_name : ()) + or next; + my @difftree = map { chomp; $_ } <$fd>; + close $fd + or next; + + # print element (entry, item) + my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit); + if ($format eq 'rss') { + print "\n" . + "" . esc_html($co{'title'}) . "\n" . + "" . esc_html($co{'author'}) . "\n" . + "$cd{'rfc2822'}\n" . + "$co_url\n" . + "" . esc_html($co_url) . "\n" . + "" . esc_html($co{'title'}) . "\n" . + "" . + "\n" . + "" . esc_html($co{'title'}) . "\n" . + "$cd{'iso-8601'}\n" . + "\n" . + " " . esc_html($co{'author_name'}) . "\n"; + if ($co{'author_email'}) { + print " " . esc_html($co{'author_email'}) . "\n"; + } + print "\n" . + # use committer for contributor + "\n" . + " " . esc_html($co{'committer_name'}) . "\n"; + if ($co{'committer_email'}) { + print " " . esc_html($co{'committer_email'}) . "\n"; + } + print "\n" . + "$cd{'iso-8601'}\n" . + "\n" . + "" . esc_html($co_url) . "\n" . + "\n" . + "
\n"; + } + my $comment = $co{'comment'}; + print "
\n";
+		foreach my $line (@$comment) {
+			$line = esc_html($line);
+			print "$line\n";
+		}
+		print "
    \n"; + foreach my $difftree_line (@difftree) { + my %difftree = parse_difftree_raw_line($difftree_line); + next if !$difftree{'from_id'}; + + my $file = $difftree{'file'} || $difftree{'to_file'}; + + print "
  • " . + "[" . + $cgi->a({-href => href(-full=>1, action=>"blobdiff", + hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'}, + hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'}, + file_name=>$file, file_parent=>$difftree{'from_file'}), + -title => "diff"}, 'D'); + if ($have_blame) { + print $cgi->a({-href => href(-full=>1, action=>"blame", + file_name=>$file, hash_base=>$commit), + -title => "blame"}, 'B'); + } + # if this is not a feed of a file history + if (!defined $file_name || $file_name ne $file) { + print $cgi->a({-href => href(-full=>1, action=>"history", + file_name=>$file, hash=>$commit), + -title => "history"}, 'H'); + } + $file = esc_path($file); + print "] ". + "$file
  • \n"; + } + if ($format eq 'rss') { + print "
]]>\n" . + "\n" . + "\n"; + } elsif ($format eq 'atom') { + print "\n
\n" . + "
\n" . + "\n"; + } + } + + # end of feed + if ($format eq 'rss') { + print "
\n\n"; + } elsif ($format eq 'atom') { + print "\n"; + } +} + +sub git_rss { + git_feed('rss'); +} + +sub git_atom { + git_feed('atom'); +} + +sub git_opml { + my @list = git_get_projects_list($project_filter, $strict_export); + if (!@list) { + die_error(404, "No projects found"); + } + + print $cgi->header( + -type => 'text/xml', + -charset => 'utf-8', + -content_disposition => 'inline; filename="opml.xml"'); + + my $title = esc_html($site_name); + my $filter = " within subdirectory "; + if (defined $project_filter) { + $filter .= esc_html($project_filter); + } else { + $filter = ""; + } + print < + + + $title OPML Export$filter + + + +XML + + foreach my $pr (@list) { + my %proj = %$pr; + my $head = git_get_head_hash($proj{'path'}); + if (!defined $head) { + next; + } + $git_dir = "$projectroot/$proj{'path'}"; + my %co = parse_commit($head); + if (!%co) { + next; + } + + my $path = esc_html(chop_str($proj{'path'}, 25, 5)); + my $rss = esc_attr(href('project' => $proj{'path'}, 'action' => 'rss', -full => 1)); + my $html = esc_attr(href('project' => $proj{'path'}, 'action' => 'summary', -full => 1)); + print "\n"; + } + print < + + +XML +} diff --git a/Git-server/Gitweb/indextext.html b/Git-server/Gitweb/indextext.html new file mode 100644 index 0000000..63ee8f3 --- /dev/null +++ b/Git-server/Gitweb/indextext.html @@ -0,0 +1,43 @@ +
+

Welcome to my software dump!

+
+

+ Here I publish stuff I make. If you came here for my blogs or other personal stuff, + you want to visit the + root domain. + If you would like to clone a project to your local machine, you can use command: +

+ +git clone git://git.dpolakovic.space/PROJECT-NAME + +

+ However, you won’t be able to push any changes back to server. + If you are interested in contributing to a project, check out the + server rules and + if they seem reasonable to you, follow this + contributor’s guide. +

+

Contact

+

+ general inquiries:   + email@dpolakovic.space +

+

+ bug reporting:        + bug-report@dpolakovic.space +

+

+ Feel free to use my + GPG key + for encryption of your messages. If you would like to encrypt your message, but + don't know how, follow this + tutorial. + I respond to emails using + inline replies. +

+ +
+
diff --git a/Git-server/Gitweb/static/gitweb.css b/Git-server/Gitweb/static/gitweb.css new file mode 100644 index 0000000..7079fbd --- /dev/null +++ b/Git-server/Gitweb/static/gitweb.css @@ -0,0 +1,707 @@ +html { + background-image: url("https://dpolakovic.space/Pictures/sky.jpg") +} + + +body { + font-family: sans-serif; + font-size: 14px; + border: solid #d9d8d1; + border-width: 1px; + /* margin: 10px; */ + width:1000px; + margin: 0 auto; + margin-bottom: 20px; + background-color: #ffffff; + color: #000000; + padding-left: 20px; + padding-right: 20px; +} + +a { + color: #0000cc; +} + +a:hover, a:visited, a:active { + color: #551a8b; +} + +a:hover { + color: #4682b4; +} + +span.cntrl { + border: dashed #aaaaaa; + border-width: 1px; + padding: 0px 2px 0px 2px; + margin: 0px 2px 0px 2px; +} + +img.logo { + float: right; + border-width: 0px; +} + +img.avatar { + vertical-align: middle; +} + +img.blob { + max-height: 100%; + max-width: 100%; +} + +a.list img.avatar { + border-style: none; +} + +div.page_header { + height: 25px; + padding: 8px; + font-size: 150%; + font-weight: bold; + background-color: #d9d8d1; +} + +div.page_header a:visited, a.header { + color: #0000cc; +} + +div.page_header a:hover { + color: #4682b4; +} + +div.page_nav { + padding: 8px; +} + +div.page_nav a:visited { + color: #551a8b; +} + +div.page_path { + padding: 8px; + font-weight: bold; + border: solid #d9d8d1; + border-width: 0px 0px 1px; +} + +div.page_footer { + height: 22px; + padding: 4px 8px; + background-color: #d9d8d1; +} + +div.page_footer_text { + line-height: 22px; + float: left; + color: #555555; + font-style: italic; +} + +div#generating_info { + margin: 4px; + font-size: smaller; + text-align: center; + color: #505050; +} + +div.page_body { + padding: 8px; + font-family: monospace; +} + +div.title, a.title { + display: block; + padding: 6px 8px; + font-weight: bold; + background-color: #edece6; + text-decoration: none; + color: #000000; +} + +div.readme { + padding: 8px; +} + +a.title:hover { + background-color: #d9d8d1; +} + +div.title_text { + padding: 6px 0px; + border: solid #d9d8d1; + border-width: 0px 0px 1px; + font-family: monospace; +} + +div.log_body { + padding: 8px 8px 8px 150px; +} + +span.age { + position: relative; + float: left; + width: 142px; + font-style: italic; +} + +span.signoff { + color: #888888; +} + +div.log_link { + padding: 0px 8px; + font-size: 70%; + font-family: sans-serif; + font-style: normal; + position: relative; + float: left; + width: 136px; +} + +div.list_head { + padding: 6px 8px 4px; + border: solid #d9d8d1; + border-width: 1px 0px 0px; + font-style: italic; +} + +.author_date, .author { + font-style: italic; +} + +div.author_date { + padding: 8px; + border: solid #d9d8d1; + border-width: 0px 0px 1px 0px; +} + +a.list { + text-decoration: none; + color: #000000; +} + +a.subject, a.name { + font-weight: bold; +} + +table.tags a.subject { + font-weight: normal; +} + +a.list:hover { + text-decoration: underline; + color: #4682b4; +} + +a.text { + text-decoration: none; + color: #0000cc; +} + +a.text:visited { + text-decoration: none; + color: #551a8b; +} + +a.text:hover { + text-decoration: underline; + color: #4682b4; +} + +table { + padding: 8px 4px; + border-spacing: 0; +} + +table.diff_tree { + font-family: monospace; +} + +table.combined.diff_tree th { + text-align: center; +} + +table.combined.diff_tree td { + padding-right: 24px; +} + +table.combined.diff_tree th.link, +table.combined.diff_tree td.link { + padding: 0px 2px; +} + +table.combined.diff_tree td.nochange a { + color: #6666ff; +} + +table.combined.diff_tree td.nochange a:hover, +table.combined.diff_tree td.nochange a:visited { + color: #d06666; +} + +table.blame { + border-collapse: collapse; +} + +table.blame td { + padding: 0px 5px; + font-size: 100%; + vertical-align: top; +} + +th { + padding: 2px 5px; + font-size: 100%; + text-align: left; +} + +/* do not change row style on hover for 'blame' view */ +tr.light, +table.blame .light:hover { + background-color: #ffffff; +} + +tr.dark, +table.blame .dark:hover { + background-color: #f6f6f0; +} + +/* currently both use the same, but it can change */ +tr.light:hover, +tr.dark:hover { + background-color: #edece6; +} + +/* boundary commits in 'blame' view */ +/* and commits without "previous" */ +tr.boundary td.sha1, +tr.no-previous td.linenr { + font-weight: bold; +} + +/* for 'blame_incremental', during processing */ +tr.color1 { background-color: #f6fff6; } +tr.color2 { background-color: #f6f6ff; } +tr.color3 { background-color: #fff6f6; } + +td { + padding: 2px 5px; + font-size: 100%; + vertical-align: top; +} + +td.link, td.selflink { + padding: 2px 5px; + font-family: sans-serif; + font-size: 70%; +} + +td.selflink { + padding-right: 0px; +} + +td.sha1 { + font-family: monospace; +} + +.error { + color: red; + background-color: yellow; +} + +td.current_head { + text-decoration: underline; +} + +td.category { + background-color: #d9d8d1; + border-top: 1px solid #000000; + border-left: 1px solid #000000; + font-weight: bold; +} + +table.diff_tree span.file_status.new { + color: #008000; +} + +table.diff_tree span.file_status.deleted { + color: #c00000; +} + +table.diff_tree span.file_status.moved, +table.diff_tree span.file_status.mode_chnge { + color: #777777; +} + +table.diff_tree span.file_status.copied { + color: #70a070; +} + +/* noage: "No commits" */ +table.project_list td.noage { + color: #808080; + font-style: italic; +} + +/* age2: 60*60*24*2 <= age */ +table.project_list td.age2, table.blame td.age2 { + font-style: italic; +} + +/* age1: 60*60*2 <= age < 60*60*24*2 */ +table.project_list td.age1 { + color: #009900; + font-style: italic; +} + +table.blame td.age1 { + color: #009900; + background: transparent; +} + +/* age0: age < 60*60*2 */ +table.project_list td.age0 { + color: #009900; + font-style: italic; + font-weight: bold; +} + +table.blame td.age0 { + color: #009900; + background: transparent; + font-weight: bold; +} + +td.pre, div.pre, div.diff { + font-family: monospace; + font-size: 12px; + white-space: pre; +} + +td.mode { + font-family: monospace; +} + +/* progress of blame_interactive */ +div#progress_bar { + height: 2px; + margin-bottom: -2px; + background-color: #d8d9d0; +} +div#progress_info { + float: right; + text-align: right; +} + +/* format of (optional) objects size in 'tree' view */ +td.size { + font-family: monospace; + text-align: right; +} + +/* styling of diffs (patchsets): commitdiff and blobdiff views */ +div.diff.header, +div.diff.extended_header { + white-space: normal; +} + +div.diff.header { + font-weight: bold; + + background-color: #edece6; + + margin-top: 4px; + padding: 4px 0px 2px 0px; + border: solid #d9d8d1; + border-width: 1px 0px 1px 0px; +} + +div.diff.header a.path { + text-decoration: underline; +} + +div.diff.extended_header, +div.diff.extended_header a.path, +div.diff.extended_header a.hash { + color: #777777; +} + +div.diff.extended_header .info { + color: #b0b0b0; +} + +div.diff.extended_header { + background-color: #f6f5ee; + padding: 2px 0px 2px 0px; +} + +div.diff a.list, +div.diff a.path, +div.diff a.hash { + text-decoration: none; +} + +div.diff a.list:hover, +div.diff a.path:hover, +div.diff a.hash:hover { + text-decoration: underline; +} + +div.diff.to_file a.path, +div.diff.to_file { + color: #007000; +} + +div.diff.add { + color: #008800; +} + +div.diff.add span.marked { + background-color: #aaffaa; +} + +div.diff.from_file a.path, +div.diff.from_file { + color: #aa0000; +} + +div.diff.rem { + color: #cc0000; +} + +div.diff.rem span.marked { + background-color: #ffaaaa; +} + +div.diff.chunk_header a, +div.diff.chunk_header { + color: #990099; +} + +div.diff.chunk_header { + border: dotted #ffe0ff; + border-width: 1px 0px 0px 0px; + margin-top: 2px; +} + +div.diff.chunk_header span.chunk_info { + background-color: #ffeeff; +} + +div.diff.chunk_header span.section { + color: #aa22aa; +} + +div.diff.incomplete { + color: #cccccc; +} + +div.diff.nodifferences { + font-weight: bold; + color: #600000; +} + +/* side-by-side diff */ +div.chunk_block { + overflow: hidden; +} + +div.chunk_block div.old { + float: left; + width: 50%; + overflow: hidden; +} + +div.chunk_block div.new { + margin-left: 50%; + width: 50%; +} + +div.chunk_block.rem div.old div.diff.rem { + background-color: #fff5f5; +} +div.chunk_block.add div.new div.diff.add { + background-color: #f8fff8; +} +div.chunk_block.chg div div.diff { + background-color: #fffff0; +} +div.chunk_block.ctx div div.diff.ctx { + color: #404040; +} + + +div.index_include { + border: solid #d9d8d1; + border-width: 0px 0px 1px; + padding: 12px 8px; +} + +div.search { + font-size: 100%; + font-weight: normal; + margin: 4px 8px; + float: right; + top: 56px; + right: 12px +} + +/* +div.projsearch { + text-align: center; + margin: 20px 0px; +} +*/ + +div.projsearch { + display: none; +} + +div.projsearch form { + margin-bottom: 2px; +} + +td.linenr { + text-align: right; +} + +a.linenr { + color: #999999; + text-decoration: none +} + +a.rss_logo { + float: right; + padding: 3px 5px; + line-height: 10px; + border: 1px solid; + border-color: #fcc7a5 #7d3302 #3e1a01 #ff954e; + color: #ffffff; + background-color: #ff6600; + font-weight: bold; + font-family: sans-serif; + font-size: 70%; + text-align: center; + text-decoration: none; +} + +a.rss_logo:hover { + background-color: #ee5500; +} + +a.rss_logo.generic { + background-color: #ff8800; +} + +a.rss_logo.generic:hover { + background-color: #ee7700; +} + +span.refs span { + padding: 0px 4px; + font-size: 70%; + font-weight: normal; + border: 1px solid; + background-color: #ffaaff; + border-color: #ffccff #ff00ee #ff00ee #ffccff; +} + +span.refs span a { + text-decoration: none; + color: inherit; +} + +span.refs span a:hover { + text-decoration: underline; +} + +span.refs span.indirect { + font-style: italic; +} + +span.refs span.ref { + background-color: #aaaaff; + border-color: #ccccff #0033cc #0033cc #ccccff; +} + +span.refs span.tag { + background-color: #ffffaa; + border-color: #ffffcc #ffee00 #ffee00 #ffffcc; +} + +span.refs span.head { + background-color: #aaffaa; + border-color: #ccffcc #00cc33 #00cc33 #ccffcc; +} + +span.atnight { + color: #cc0000; +} + +span.match { + color: #e00000; +} + +div.binary { + font-style: italic; +} + +div.remote { + margin: .5em; + border: 1px solid #d9d8d1; + display: inline-block; +} + +/* JavaScript-based timezone manipulation */ + +.popup { /* timezone selection UI */ + position: absolute; + /* "top: 0; right: 0;" would be better, if not for bugs in browsers */ + top: 0; left: 0; + border: 1px solid; + padding: 2px; + background-color: #f0f0f0; + font-style: normal; + color: #000000; + cursor: auto; +} + +.close-button { /* close timezone selection UI without selecting */ + /* float doesn't work within absolutely positioned container, + * if width of container is not set explicitly */ + /* float: right; */ + position: absolute; + top: 0px; right: 0px; + border: 1px solid green; + margin: 1px 1px 1px 1px; + padding-bottom: 2px; + width: 12px; + height: 10px; + font-size: 9px; + font-weight: bold; + text-align: center; + background-color: #fff0f0; + cursor: pointer; +} + + +/* Style definition generated by highlight 2.4.5, http://andre-simon.de/doku/highlight/en/highlight.php */ + +/* Highlighting theme definition: */ + +.num { color:#2928ff; } +.esc { color:#ff00ff; } +.str { color:#ff0000; } +.dstr { color:#818100; } +.slc { color:#838183; font-style:italic; } +.com { color:#838183; font-style:italic; } +.dir { color:#008200; } +.sym { color:#000000; } +.line { color:#555555; } +.kwa { color:#000000; font-weight:bold; } +.kwb { color:#830000; } +.kwc { color:#000000; font-weight:bold; } +.kwd { color:#010181; } + diff --git a/Git-server/Gitweb/static/gitweb.js b/Git-server/Gitweb/static/gitweb.js new file mode 100644 index 0000000..e676aae --- /dev/null +++ b/Git-server/Gitweb/static/gitweb.js @@ -0,0 +1,1579 @@ +// Copyright (C) 2007, Fredrik Kuivinen +// 2007, Petr Baudis +// 2008-2011, Jakub Narebski + +/** + * @fileOverview Generic JavaScript code (helper functions) + * @license GPLv2 or later + */ + + +/* ============================================================ */ +/* ............................................................ */ +/* Padding */ + +/** + * pad INPUT on the left with STR that is assumed to have visible + * width of single character (for example nonbreakable spaces), + * to WIDTH characters + * + * example: padLeftStr(12, 3, '\u00A0') == '\u00A012' + * ('\u00A0' is nonbreakable space) + * + * @param {Number|String} input: number to pad + * @param {Number} width: visible width of output + * @param {String} str: string to prefix to string, defaults to '\u00A0' + * @returns {String} INPUT prefixed with STR x (WIDTH - INPUT.length) + */ +function padLeftStr(input, width, str) { + var prefix = ''; + if (typeof str === 'undefined') { + ch = '\u00A0'; // using ' ' doesn't work in all browsers + } + + width -= input.toString().length; + while (width > 0) { + prefix += str; + width--; + } + return prefix + input; +} + +/** + * Pad INPUT on the left to WIDTH, using given padding character CH, + * for example padLeft('a', 3, '_') is '__a' + * padLeft(4, 2) is '04' (same as padLeft(4, 2, '0')) + * + * @param {String} input: input value converted to string. + * @param {Number} width: desired length of output. + * @param {String} ch: single character to prefix to string, defaults to '0'. + * + * @returns {String} Modified string, at least SIZE length. + */ +function padLeft(input, width, ch) { + var s = input + ""; + if (typeof ch === 'undefined') { + ch = '0'; + } + + while (s.length < width) { + s = ch + s; + } + return s; +} + + +/* ............................................................ */ +/* Handling browser incompatibilities */ + +/** + * Create XMLHttpRequest object in cross-browser way + * @returns XMLHttpRequest object, or null + */ +function createRequestObject() { + try { + return new XMLHttpRequest(); + } catch (e) {} + try { + return window.createRequest(); + } catch (e) {} + try { + return new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) {} + try { + return new ActiveXObject("Microsoft.XMLHTTP"); + } catch (e) {} + + return null; +} + + +/** + * Insert rule giving specified STYLE to given SELECTOR at the end of + * first CSS stylesheet. + * + * @param {String} selector: CSS selector, e.g. '.class' + * @param {String} style: rule contents, e.g. 'background-color: red;' + */ +function addCssRule(selector, style) { + var stylesheet = document.styleSheets[0]; + + var theRules = []; + if (stylesheet.cssRules) { // W3C way + theRules = stylesheet.cssRules; + } else if (stylesheet.rules) { // IE way + theRules = stylesheet.rules; + } + + if (stylesheet.insertRule) { // W3C way + stylesheet.insertRule(selector + ' { ' + style + ' }', theRules.length); + } else if (stylesheet.addRule) { // IE way + stylesheet.addRule(selector, style); + } +} + + +/* ............................................................ */ +/* Support for legacy browsers */ + +/** + * Provides getElementsByClassName method, if there is no native + * implementation of this method. + * + * NOTE that there are limits and differences compared to native + * getElementsByClassName as defined by e.g.: + * https://developer.mozilla.org/en/DOM/document.getElementsByClassName + * https://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#dom-getelementsbyclassname + * https://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#dom-document-getelementsbyclassname + * + * Namely, this implementation supports only single class name as + * argument and not set of space-separated tokens representing classes, + * it returns Array of nodes rather than live NodeList, and has + * additional optional argument where you can limit search to given tags + * (via getElementsByTagName). + * + * Based on + * https://code.google.com/p/getelementsbyclassname/ + * http://www.dustindiaz.com/getelementsbyclass/ + * https://stackoverflow.com/questions/1818865/do-we-have-getelementsbyclassname-in-javascript + * + * See also https://johnresig.com/blog/getelementsbyclassname-speed-comparison/ + * + * @param {String} class: name of _single_ class to find + * @param {String} [taghint] limit search to given tags + * @returns {Node[]} array of matching elements + */ +if (!('getElementsByClassName' in document)) { + document.getElementsByClassName = function (classname, taghint) { + taghint = taghint || "*"; + var elements = (taghint === "*" && document.all) ? + document.all : + document.getElementsByTagName(taghint); + var pattern = new RegExp("(^|\\s)" + classname + "(\\s|$)"); + var matches= []; + for (var i = 0, j = 0, n = elements.length; i < n; i++) { + var el= elements[i]; + if (el.className && pattern.test(el.className)) { + // matches.push(el); + matches[j] = el; + j++; + } + } + return matches; + }; +} // end if + + +/* ............................................................ */ +/* unquoting/unescaping filenames */ + +/**#@+ + * @constant + */ +var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g; +var octEscRe = /^[0-7]{1,3}$/; +var maybeQuotedRe = /^\"(.*)\"$/; +/**#@-*/ + +/** + * unquote maybe C-quoted filename (as used by git, i.e. it is + * in double quotes '"' if there is any escape character used) + * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a' + * + * @param {String} str: git-quoted string + * @returns {String} Unquoted and unescaped string + * + * @globals escCodeRe, octEscRe, maybeQuotedRe + */ +function unquote(str) { + function unq(seq) { + var es = { + // character escape codes, aka escape sequences (from C) + // replacements are to some extent JavaScript specific + t: "\t", // tab (HT, TAB) + n: "\n", // newline (NL) + r: "\r", // return (CR) + f: "\f", // form feed (FF) + b: "\b", // backspace (BS) + a: "\x07", // alarm (bell) (BEL) + e: "\x1B", // escape (ESC) + v: "\v" // vertical tab (VT) + }; + + if (seq.search(octEscRe) !== -1) { + // octal char sequence + return String.fromCharCode(parseInt(seq, 8)); + } else if (seq in es) { + // C escape sequence, aka character escape code + return es[seq]; + } + // quoted ordinary character + return seq; + } + + var match = str.match(maybeQuotedRe); + if (match) { + str = match[1]; + // perhaps str = eval('"'+str+'"'); would be enough? + str = str.replace(escCodeRe, + function (substr, p1, offset, s) { return unq(p1); }); + } + return str; +} + +/* end of common-lib.js */ +// Copyright (C) 2007, Fredrik Kuivinen +// 2007, Petr Baudis +// 2008-2011, Jakub Narebski + +/** + * @fileOverview Datetime manipulation: parsing and formatting + * @license GPLv2 or later + */ + + +/* ............................................................ */ +/* parsing and retrieving datetime related information */ + +/** + * used to extract hours and minutes from timezone info, e.g '-0900' + * @constant + */ +var tzRe = /^([+\-])([0-9][0-9])([0-9][0-9])$/; + +/** + * convert numeric timezone +/-ZZZZ to offset from UTC in seconds + * + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' + * @returns {Number} offset from UTC in seconds for timezone + * + * @globals tzRe + */ +function timezoneOffset(timezoneInfo) { + var match = tzRe.exec(timezoneInfo); + var tz_sign = (match[1] === '-' ? -1 : +1); + var tz_hour = parseInt(match[2],10); + var tz_min = parseInt(match[3],10); + + return tz_sign*(((tz_hour*60) + tz_min)*60); +} + +/** + * return local (browser) timezone as offset from UTC in seconds + * + * @returns {Number} offset from UTC in seconds for local timezone + */ +function localTimezoneOffset() { + // getTimezoneOffset returns the time-zone offset from UTC, + // in _minutes_, for the current locale + return ((new Date()).getTimezoneOffset() * -60); +} + +/** + * return local (browser) timezone as numeric timezone '(+|-)HHMM' + * + * @returns {String} locat timezone as -/+ZZZZ + */ +function localTimezoneInfo() { + var tzOffsetMinutes = (new Date()).getTimezoneOffset() * -1; + + return formatTimezoneInfo(0, tzOffsetMinutes); +} + + +/** + * Parse RFC-2822 date into a Unix timestamp (into epoch) + * + * @param {String} date: date in RFC-2822 format, e.g. 'Thu, 21 Dec 2000 16:01:07 +0200' + * @returns {Number} epoch i.e. seconds since '00:00:00 1970-01-01 UTC' + */ +function parseRFC2822Date(date) { + // Date.parse accepts the IETF standard (RFC 1123 Section 5.2.14 and elsewhere) + // date syntax, which is defined in RFC 2822 (obsoletes RFC 822) + // and returns number of _milli_seconds since January 1, 1970, 00:00:00 UTC + return Date.parse(date) / 1000; +} + + +/* ............................................................ */ +/* formatting date */ + +/** + * format timezone offset as numerical timezone '(+|-)HHMM' or '(+|-)HH:MM' + * + * @param {Number} hours: offset in hours, e.g. 2 for '+0200' + * @param {Number} [minutes] offset in minutes, e.g. 30 for '-4030'; + * it is split into hours if not 0 <= minutes < 60, + * for example 1200 would give '+0100'; + * defaults to 0 + * @param {String} [sep] separator between hours and minutes part, + * default is '', might be ':' for W3CDTF (rfc-3339) + * @returns {String} timezone in '(+|-)HHMM' or '(+|-)HH:MM' format + */ +function formatTimezoneInfo(hours, minutes, sep) { + minutes = minutes || 0; // to be able to use formatTimezoneInfo(hh) + sep = sep || ''; // default format is +/-ZZZZ + + if (minutes < 0 || minutes > 59) { + hours = minutes > 0 ? Math.floor(minutes / 60) : Math.ceil(minutes / 60); + minutes = Math.abs(minutes - 60*hours); // sign of minutes is sign of hours + // NOTE: this works correctly because there is no UTC-00:30 timezone + } + + var tzSign = hours >= 0 ? '+' : '-'; + if (hours < 0) { + hours = -hours; // sign is stored in tzSign + } + + return tzSign + padLeft(hours, 2, '0') + sep + padLeft(minutes, 2, '0'); +} + +/** + * translate 'utc' and 'local' to numerical timezone + * @param {String} timezoneInfo: might be 'utc' or 'local' (browser) + */ +function normalizeTimezoneInfo(timezoneInfo) { + switch (timezoneInfo) { + case 'utc': + return '+0000'; + case 'local': // 'local' is browser timezone + return localTimezoneInfo(); + } + return timezoneInfo; +} + + +/** + * return date in local time formatted in iso-8601 like format + * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' + * + * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' + * @returns {String} date in local time in iso-8601 like format + */ +function formatDateISOLocal(epoch, timezoneInfo) { + // date corrected by timezone + var localDate = new Date(1000 * (epoch + + timezoneOffset(timezoneInfo))); + var localDateStr = // e.g. '2005-08-07' + localDate.getUTCFullYear() + '-' + + padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + + padLeft(localDate.getUTCDate(), 2, '0'); + var localTimeStr = // e.g. '21:49:46' + padLeft(localDate.getUTCHours(), 2, '0') + ':' + + padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + + padLeft(localDate.getUTCSeconds(), 2, '0'); + + return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; +} + +/** + * return date in local time formatted in rfc-2822 format + * e.g. 'Thu, 21 Dec 2000 16:01:07 +0200' + * + * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' + * @param {Boolean} [padDay] e.g. 'Sun, 07 Aug' if true, 'Sun, 7 Aug' otherwise + * @returns {String} date in local time in rfc-2822 format + */ +function formatDateRFC2882(epoch, timezoneInfo, padDay) { + // A short textual representation of a month, three letters + var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + // A textual representation of a day, three letters + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + // date corrected by timezone + var localDate = new Date(1000 * (epoch + + timezoneOffset(timezoneInfo))); + var localDateStr = // e.g. 'Sun, 7 Aug 2005' or 'Sun, 07 Aug 2005' + days[localDate.getUTCDay()] + ', ' + + (padDay ? padLeft(localDate.getUTCDate(),2,'0') : localDate.getUTCDate()) + ' ' + + months[localDate.getUTCMonth()] + ' ' + + localDate.getUTCFullYear(); + var localTimeStr = // e.g. '21:49:46' + padLeft(localDate.getUTCHours(), 2, '0') + ':' + + padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + + padLeft(localDate.getUTCSeconds(), 2, '0'); + + return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; +} + +/* end of datetime.js */ +/** + * @fileOverview Accessing cookies from JavaScript + * @license GPLv2 or later + */ + +/* + * Based on subsection "Cookies in JavaScript" of "Professional + * JavaScript for Web Developers" by Nicholas C. Zakas and cookie + * plugin from jQuery (dual licensed under the MIT and GPL licenses) + */ + + +/** + * Create a cookie with the given name and value, + * and other optional parameters. + * + * @example + * setCookie('foo', 'bar'); // will be deleted when browser exits + * setCookie('foo', 'bar', { expires: new Date(Date.parse('Jan 1, 2012')) }); + * setCookie('foo', 'bar', { expires: 7 }); // 7 days = 1 week + * setCookie('foo', 'bar', { expires: 14, path: '/' }); + * + * @param {String} sName: Unique name of a cookie (letters, numbers, underscores). + * @param {String} sValue: The string value stored in a cookie. + * @param {Object} [options] An object literal containing key/value pairs + * to provide optional cookie attributes. + * @param {String|Number|Date} [options.expires] Either literal string to be used as cookie expires, + * or an integer specifying the expiration date from now on in days, + * or a Date object to be used as cookie expiration date. + * If a negative value is specified or a date in the past), + * the cookie will be deleted. + * If set to null or omitted, the cookie will be a session cookie + * and will not be retained when the browser exits. + * @param {String} [options.path] Restrict access of a cookie to particular directory + * (default: path of page that created the cookie). + * @param {String} [options.domain] Override what web sites are allowed to access cookie + * (default: domain of page that created the cookie). + * @param {Boolean} [options.secure] If true, the secure attribute of the cookie will be set + * and the cookie would be accessible only from secure sites + * (cookie transmission will require secure protocol like HTTPS). + */ +function setCookie(sName, sValue, options) { + options = options || {}; + if (sValue === null) { + sValue = ''; + option.expires = 'delete'; + } + + var sCookie = sName + '=' + encodeURIComponent(sValue); + + if (options.expires) { + var oExpires = options.expires, sDate; + if (oExpires === 'delete') { + sDate = 'Thu, 01 Jan 1970 00:00:00 GMT'; + } else if (typeof oExpires === 'string') { + sDate = oExpires; + } else { + var oDate; + if (typeof oExpires === 'number') { + oDate = new Date(); + oDate.setTime(oDate.getTime() + (oExpires * 24 * 60 * 60 * 1000)); // days to ms + } else { + oDate = oExpires; + } + sDate = oDate.toGMTString(); + } + sCookie += '; expires=' + sDate; + } + + if (options.path) { + sCookie += '; path=' + (options.path); + } + if (options.domain) { + sCookie += '; domain=' + (options.domain); + } + if (options.secure) { + sCookie += '; secure'; + } + document.cookie = sCookie; +} + +/** + * Get the value of a cookie with the given name. + * + * @param {String} sName: Unique name of a cookie (letters, numbers, underscores) + * @returns {String|null} The string value stored in a cookie + */ +function getCookie(sName) { + var sRE = '(?:; )?' + sName + '=([^;]*);?'; + var oRE = new RegExp(sRE); + if (oRE.test(document.cookie)) { + return decodeURIComponent(RegExp['$1']); + } else { + return null; + } +} + +/** + * Delete cookie with given name + * + * @param {String} sName: Unique name of a cookie (letters, numbers, underscores) + * @param {Object} [options] An object literal containing key/value pairs + * to provide optional cookie attributes. + * @param {String} [options.path] Must be the same as when setting a cookie + * @param {String} [options.domain] Must be the same as when setting a cookie + */ +function deleteCookie(sName, options) { + options = options || {}; + options.expires = 'delete'; + + setCookie(sName, '', options); +} + +/* end of cookies.js */ +// Copyright (C) 2007, Fredrik Kuivinen +// 2007, Petr Baudis +// 2008-2011, Jakub Narebski + +/** + * @fileOverview Detect if JavaScript is enabled, and pass it to server-side + * @license GPLv2 or later + */ + + +/* ============================================================ */ +/* Manipulating links */ + +/** + * used to check if link has 'js' query parameter already (at end), + * and other reasons to not add 'js=1' param at the end of link + * @constant + */ +var jsExceptionsRe = /[;?]js=[01](#.*)?$/; + +/** + * Add '?js=1' or ';js=1' to the end of every link in the document + * that doesn't have 'js' query parameter set already. + * + * Links with 'js=1' lead to JavaScript version of given action, if it + * exists (currently there is only 'blame_incremental' for 'blame') + * + * To be used as `window.onload` handler + * + * @globals jsExceptionsRe + */ +function fixLinks() { + var allLinks = document.getElementsByTagName("a") || document.links; + for (var i = 0, len = allLinks.length; i < len; i++) { + var link = allLinks[i]; + if (!jsExceptionsRe.test(link)) { + link.href = link.href.replace(/(#|$)/, + (link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1$1'); + } + } +} + +/* end of javascript-detection.js */ +// Copyright (C) 2011, John 'Warthog9' Hawley +// 2011, Jakub Narebski + +/** + * @fileOverview Manipulate dates in gitweb output, adjusting timezone + * @license GPLv2 or later + */ + +/** + * Get common timezone, add UI for changing timezones, and adjust + * dates to use requested common timezone. + * + * This function is called during onload event (added to window.onload). + * + * @param {String} tzDefault: default timezone, if there is no cookie + * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone + * @param {String} tzCookieInfo.name: name of cookie to store timezone + * @param {String} tzClassName: denotes elements with date to be adjusted + */ +function onloadTZSetup(tzDefault, tzCookieInfo, tzClassName) { + var tzCookieTZ = getCookie(tzCookieInfo.name, tzCookieInfo); + var tz = tzDefault; + + if (tzCookieTZ) { + // set timezone to value saved in a cookie + tz = tzCookieTZ; + // refresh cookie, so its expiration counts from last use of gitweb + setCookie(tzCookieInfo.name, tzCookieTZ, tzCookieInfo); + } + + // add UI for changing timezone + addChangeTZ(tz, tzCookieInfo, tzClassName); + + // server-side of gitweb produces datetime in UTC, + // so if tz is 'utc' there is no need for changes + var nochange = tz === 'utc'; + + // adjust dates to use specified common timezone + fixDatetimeTZ(tz, tzClassName, nochange); +} + + +/* ...................................................................... */ +/* Changing dates to use requested timezone */ + +/** + * Replace RFC-2822 dates contained in SPAN elements with tzClassName + * CSS class with equivalent dates in given timezone. + * + * @param {String} tz: numeric timezone in '(-|+)HHMM' format, or 'utc', or 'local' + * @param {String} tzClassName: specifies elements to be changed + * @param {Boolean} nochange: markup for timezone change, but don't change it + */ +function fixDatetimeTZ(tz, tzClassName, nochange) { + // sanity check, method should be ensured by common-lib.js + if (!document.getElementsByClassName) { + return; + } + + // translate to timezone in '(-|+)HHMM' format + tz = normalizeTimezoneInfo(tz); + + // NOTE: result of getElementsByClassName should probably be cached + var classesFound = document.getElementsByClassName(tzClassName, "span"); + for (var i = 0, len = classesFound.length; i < len; i++) { + var curElement = classesFound[i]; + + curElement.title = 'Click to change timezone'; + if (!nochange) { + // we use *.firstChild.data (W3C DOM) instead of *.innerHTML + // as the latter doesn't always work everywhere in every browser + var epoch = parseRFC2822Date(curElement.firstChild.data); + var adjusted = formatDateRFC2882(epoch, tz); + + curElement.firstChild.data = adjusted; + } + } +} + + +/* ...................................................................... */ +/* Adding triggers, generating timezone menu, displaying and hiding */ + +/** + * Adds triggers for UI to change common timezone used for dates in + * gitweb output: it marks up and/or creates item to click to invoke + * timezone change UI, creates timezone UI fragment to be attached, + * and installs appropriate onclick trigger (via event delegation). + * + * @param {String} tzSelected: pre-selected timezone, + * 'utc' or 'local' or '(-|+)HHMM' + * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone + * @param {String} tzClassName: specifies elements to install trigger + */ +function addChangeTZ(tzSelected, tzCookieInfo, tzClassName) { + // make link to timezone UI discoverable + addCssRule('.'+tzClassName + ':hover', + 'text-decoration: underline; cursor: help;'); + + // create form for selecting timezone (to be saved in a cookie) + var tzSelectFragment = document.createDocumentFragment(); + tzSelectFragment = createChangeTZForm(tzSelectFragment, + tzSelected, tzCookieInfo, tzClassName); + + // event delegation handler for timezone selection UI (clicking on entry) + // see http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/ + // assumes that there is no existing document.onclick handler + document.onclick = function onclickHandler(event) { + //IE doesn't pass in the event object + event = event || window.event; + + //IE uses srcElement as the target + var target = event.target || event.srcElement; + + switch (target.className) { + case tzClassName: + // don't display timezone menu if it is already displayed + if (tzSelectFragment.childNodes.length > 0) { + displayChangeTZForm(target, tzSelectFragment); + } + break; + } // end switch + }; +} + +/** + * Create DocumentFragment with UI for changing common timezone in + * which dates are shown in. + * + * @param {DocumentFragment} documentFragment: where attach UI + * @param {String} tzSelected: default (pre-selected) timezone + * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone + * @returns {DocumentFragment} + */ +function createChangeTZForm(documentFragment, tzSelected, tzCookieInfo, tzClassName) { + var div = document.createElement("div"); + div.className = 'popup'; + + /* '
X
' */ + var closeButton = document.createElement('div'); + closeButton.className = 'close-button'; + closeButton.title = '(click on this box to close)'; + closeButton.appendChild(document.createTextNode('X')); + closeButton.onclick = closeTZFormHandler(documentFragment, tzClassName); + div.appendChild(closeButton); + + /* 'Select timezone:
' */ + div.appendChild(document.createTextNode('Select timezone: ')); + var br = document.createElement('br'); + br.clear = 'all'; + div.appendChild(br); + + /* '' */ + var select = document.createElement("select"); + select.name = "tzoffset"; + //select.style.clear = 'all'; + select.appendChild(generateTZOptions(tzSelected)); + select.onchange = selectTZHandler(documentFragment, tzCookieInfo, tzClassName); + div.appendChild(select); + + documentFragment.appendChild(div); + + return documentFragment; +} + + +/** + * Hide (remove from DOM) timezone change UI, ensuring that it is not + * garbage collected and that it can be re-enabled later. + * + * @param {DocumentFragment} documentFragment: contains detached UI + * @param {HTMLSelectElement} target: select element inside of UI + * @param {String} tzClassName: specifies element where UI was installed + * @returns {DocumentFragment} documentFragment + */ +function removeChangeTZForm(documentFragment, target, tzClassName) { + // find containing element, where we appended timezone selection UI + // `target' is somewhere inside timezone menu + var container = target.parentNode, popup = target; + while (container && + container.className !== tzClassName) { + popup = container; + container = container.parentNode; + } + // safety check if we found correct container, + // and if it isn't deleted already + if (!container || !popup || + container.className !== tzClassName || + popup.className !== 'popup') { + return documentFragment; + } + + // timezone selection UI was appended as last child + // see also displayChangeTZForm function + var removed = popup.parentNode.removeChild(popup); + if (documentFragment.firstChild !== removed) { // the only child + // re-append it so it would be available for next time + documentFragment.appendChild(removed); + } + // all of inline style was added by this script + // it is not really needed to remove it, but it is a good practice + container.removeAttribute('style'); + + return documentFragment; +} + + +/** + * Display UI for changing common timezone for dates in gitweb output. + * To be used from 'onclick' event handler. + * + * @param {HTMLElement} target: where to install/display UI + * @param {DocumentFragment} tzSelectFragment: timezone selection UI + */ +function displayChangeTZForm(target, tzSelectFragment) { + // for absolute positioning to be related to target element + target.style.position = 'relative'; + target.style.display = 'inline-block'; + + // show/display UI for changing timezone + target.appendChild(tzSelectFragment); +} + + +/* ...................................................................... */ +/* List of timezones for timezone selection menu */ + +/** + * Generate list of timezones for creating timezone select UI + * + * @returns {Object[]} list of e.g. { value: '+0100', descr: 'GMT+01:00' } + */ +function generateTZList() { + var timezones = [ + { value: "utc", descr: "UTC/GMT"}, + { value: "local", descr: "Local (per browser)"} + ]; + + // generate all full hour timezones (no fractional timezones) + for (var x = -12, idx = timezones.length; x <= +14; x++, idx++) { + var hours = (x >= 0 ? '+' : '-') + padLeft(x >=0 ? x : -x, 2); + timezones[idx] = { value: hours + '00', descr: 'UTC' + hours + ':00'}; + if (x === 0) { + timezones[idx].descr = 'UTC\u00B100:00'; // 'UTC±00:00' + } + } + + return timezones; +} + +/** + * Generate elements for timezone select UI + * + * @param {String} tzSelected: default timezone + * @returns {DocumentFragment} list of options elements to appendChild + */ +function generateTZOptions(tzSelected) { + var elems = document.createDocumentFragment(); + var timezones = generateTZList(); + + for (var i = 0, len = timezones.length; i < len; i++) { + var tzone = timezones[i]; + var option = document.createElement("option"); + if (tzone.value === tzSelected) { + option.defaultSelected = true; + } + option.value = tzone.value; + option.appendChild(document.createTextNode(tzone.descr)); + + elems.appendChild(option); + } + + return elems; +} + + +/* ...................................................................... */ +/* Event handlers and/or their generators */ + +/** + * Create event handler that select timezone and closes timezone select UI. + * To be used as $('select[name="tzselect"]').onchange handler. + * + * @param {DocumentFragment} tzSelectFragment: timezone selection UI + * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone + * @param {String} tzCookieInfo.name: name of cookie to save result of selection + * @param {String} tzClassName: specifies element where UI was installed + * @returns {Function} event handler + */ +function selectTZHandler(tzSelectFragment, tzCookieInfo, tzClassName) { + //return function selectTZ(event) { + return function (event) { + event = event || window.event; + var target = event.target || event.srcElement; + + var selected = target.options.item(target.selectedIndex); + removeChangeTZForm(tzSelectFragment, target, tzClassName); + + if (selected) { + selected.defaultSelected = true; + setCookie(tzCookieInfo.name, selected.value, tzCookieInfo); + fixDatetimeTZ(selected.value, tzClassName); + } + }; +} + +/** + * Create event handler that closes timezone select UI. + * To be used e.g. as $('.closebutton').onclick handler. + * + * @param {DocumentFragment} tzSelectFragment: timezone selection UI + * @param {String} tzClassName: specifies element where UI was installed + * @returns {Function} event handler + */ +function closeTZFormHandler(tzSelectFragment, tzClassName) { + //return function closeTZForm(event) { + return function (event) { + event = event || window.event; + var target = event.target || event.srcElement; + + removeChangeTZForm(tzSelectFragment, target, tzClassName); + }; +} + +/* end of adjust-timezone.js */ +// Copyright (C) 2007, Fredrik Kuivinen +// 2007, Petr Baudis +// 2008-2011, Jakub Narebski + +/** + * @fileOverview JavaScript side of Ajax-y 'blame_incremental' view in gitweb + * @license GPLv2 or later + */ + +/* ============================================================ */ +/* + * This code uses DOM methods instead of (nonstandard) innerHTML + * to modify page. + * + * innerHTML is non-standard IE extension, though supported by most + * browsers; however Firefox up to version 1.5 didn't implement it in + * a strict mode (application/xml+xhtml mimetype). + * + * Also my simple benchmarks show that using elem.firstChild.data = + * 'content' is slightly faster than elem.innerHTML = 'content'. It + * is however more fragile (text element fragment must exists), and + * less feature-rich (we cannot add HTML). + * + * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the + * equivalent using DOM 2 Core is usually shown in comments. + */ + + +/* ............................................................ */ +/* utility/helper functions (and variables) */ + +var projectUrl; // partial query + separator ('?' or ';') + +// 'commits' is an associative map. It maps SHA1s to Commit objects. +var commits = {}; + +/** + * constructor for Commit objects, used in 'blame' + * @class Represents a blamed commit + * @param {String} sha1: SHA-1 identifier of a commit + */ +function Commit(sha1) { + if (this instanceof Commit) { + this.sha1 = sha1; + this.nprevious = 0; /* number of 'previous', effective parents */ + } else { + return new Commit(sha1); + } +} + +/* ............................................................ */ +/* progress info, timing, error reporting */ + +var blamedLines = 0; +var totalLines = '???'; +var div_progress_bar; +var div_progress_info; + +/** + * Detects how many lines does a blamed file have, + * This information is used in progress info + * + * @returns {Number|String} Number of lines in file, or string '...' + */ +function countLines() { + var table = + document.getElementById('blame_table') || + document.getElementsByTagName('table')[0]; + + if (table) { + return table.getElementsByTagName('tr').length - 1; // for header + } else { + return '...'; + } +} + +/** + * update progress info and length (width) of progress bar + * + * @globals div_progress_info, div_progress_bar, blamedLines, totalLines + */ +function updateProgressInfo() { + if (!div_progress_info) { + div_progress_info = document.getElementById('progress_info'); + } + if (!div_progress_bar) { + div_progress_bar = document.getElementById('progress_bar'); + } + if (!div_progress_info && !div_progress_bar) { + return; + } + + var percentage = Math.floor(100.0*blamedLines/totalLines); + + if (div_progress_info) { + div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines + + ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)'; + } + + if (div_progress_bar) { + //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;'); + div_progress_bar.style.width = percentage + '%'; + } +} + + +var t_interval_server = ''; +var cmds_server = ''; +var t0 = new Date(); + +/** + * write how much it took to generate data, and to run script + * + * @globals t0, t_interval_server, cmds_server + */ +function writeTimeInterval() { + var info_time = document.getElementById('generating_time'); + if (!info_time || !t_interval_server) { + return; + } + var t1 = new Date(); + info_time.firstChild.data += ' + (' + + t_interval_server + ' sec server blame_data / ' + + (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)'; + + var info_cmds = document.getElementById('generating_cmd'); + if (!info_time || !cmds_server) { + return; + } + info_cmds.firstChild.data += ' + ' + cmds_server; +} + +/** + * show an error message alert to user within page (in progress info area) + * @param {String} str: plain text error message (no HTML) + * + * @globals div_progress_info + */ +function errorInfo(str) { + if (!div_progress_info) { + div_progress_info = document.getElementById('progress_info'); + } + if (div_progress_info) { + div_progress_info.className = 'error'; + div_progress_info.firstChild.data = str; + } +} + +/* ............................................................ */ +/* coloring rows during blame_data (git blame --incremental) run */ + +/** + * used to extract N from 'colorN', where N is a number, + * @constant + */ +var colorRe = /\bcolor([0-9]*)\b/; + +/** + * return N if , otherwise return null + * (some browsers require CSS class names to begin with letter) + * + * @param {HTMLElement} tr: table row element to check + * @param {String} tr.className: 'class' attribute of tr element + * @returns {Number|null} N if tr.className == 'colorN', otherwise null + * + * @globals colorRe + */ +function getColorNo(tr) { + if (!tr) { + return null; + } + var className = tr.className; + if (className) { + var match = colorRe.exec(className); + if (match) { + return parseInt(match[1], 10); + } + } + return null; +} + +var colorsFreq = [0, 0, 0]; +/** + * return one of given possible colors (currently least used one) + * example: chooseColorNoFrom(2, 3) returns 2 or 3 + * + * @param {Number[]} arguments: one or more numbers + * assumes that 1 <= arguments[i] <= colorsFreq.length + * @returns {Number} Least used color number from arguments + * @globals colorsFreq + */ +function chooseColorNoFrom() { + // choose the color which is least used + var colorNo = arguments[0]; + for (var i = 1; i < arguments.length; i++) { + if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) { + colorNo = arguments[i]; + } + } + colorsFreq[colorNo-1]++; + return colorNo; +} + +/** + * given two neighbor elements, find color which would be different + * from color of both of neighbors; used to 3-color blame table + * + * @param {HTMLElement} tr_prev + * @param {HTMLElement} tr_next + * @returns {Number} color number N such that + * colorN != tr_prev.className && colorN != tr_next.className + */ +function findColorNo(tr_prev, tr_next) { + var color_prev = getColorNo(tr_prev); + var color_next = getColorNo(tr_next); + + + // neither of neighbors has color set + // THEN we can use any of 3 possible colors + if (!color_prev && !color_next) { + return chooseColorNoFrom(1,2,3); + } + + // either both neighbors have the same color, + // or only one of neighbors have color set + // THEN we can use any color except given + var color; + if (color_prev === color_next) { + color = color_prev; // = color_next; + } else if (!color_prev) { + color = color_next; + } else if (!color_next) { + color = color_prev; + } + if (color) { + return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1); + } + + // neighbors have different colors + // THEN there is only one color left + return (3 - ((color_prev + color_next) % 3)); +} + +/* ............................................................ */ +/* coloring rows like 'blame' after 'blame_data' finishes */ + +/** + * returns true if given row element (tr) is first in commit group + * to be used only after 'blame_data' finishes (after processing) + * + * @param {HTMLElement} tr: table row + * @returns {Boolean} true if TR is first in commit group + */ +function isStartOfGroup(tr) { + return tr.firstChild.className === 'sha1'; +} + +/** + * change colors to use zebra coloring (2 colors) instead of 3 colors + * concatenate neighbor commit groups belonging to the same commit + * + * @globals colorRe + */ +function fixColorsAndGroups() { + var colorClasses = ['light', 'dark']; + var linenum = 1; + var tr, prev_group; + var colorClass = 0; + var table = + document.getElementById('blame_table') || + document.getElementsByTagName('table')[0]; + + while ((tr = document.getElementById('l'+linenum))) { + // index origin is 0, which is table header; start from 1 + //while ((tr = table.rows[linenum])) { // <- it is slower + if (isStartOfGroup(tr, linenum, document)) { + if (prev_group && + prev_group.firstChild.firstChild.href === + tr.firstChild.firstChild.href) { + // we have to concatenate groups + var prev_rows = prev_group.firstChild.rowSpan || 1; + var curr_rows = tr.firstChild.rowSpan || 1; + prev_group.firstChild.rowSpan = prev_rows + curr_rows; + //tr.removeChild(tr.firstChild); + tr.deleteCell(0); // DOM2 HTML way + } else { + colorClass = (colorClass + 1) % 2; + prev_group = tr; + } + } + var tr_class = tr.className; + tr.className = tr_class.replace(colorRe, colorClasses[colorClass]); + linenum++; + } +} + + +/* ============================================================ */ +/* main part: parsing response */ + +/** + * Function called for each blame entry, as soon as it finishes. + * It updates page via DOM manipulation, adding sha1 info, etc. + * + * @param {Commit} commit: blamed commit + * @param {Object} group: object representing group of lines, + * which blame the same commit (blame entry) + * + * @globals blamedLines + */ +function handleLine(commit, group) { + /* + This is the structure of the HTML fragment we are working + with: + + + + 123 + # times (my ext3 doesn't). + + */ + + var resline = group.resline; + + // format date and time string only once per commit + if (!commit.info) { + /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */ + commit.info = commit.author + ', ' + + formatDateISOLocal(commit.authorTime, commit.authorTimezone); + } + + // color depends on group of lines, not only on blamed commit + var colorNo = findColorNo( + document.getElementById('l'+(resline-1)), + document.getElementById('l'+(resline+group.numlines)) + ); + + // loop over lines in commit group + for (var i = 0; i < group.numlines; i++, resline++) { + var tr = document.getElementById('l'+resline); + if (!tr) { + break; + } + /* + + + 123 + # times (my ext3 doesn't). + + */ + var td_sha1 = tr.firstChild; + var a_sha1 = td_sha1.firstChild; + var a_linenr = td_sha1.nextSibling.firstChild; + + /* */ + var tr_class = ''; + if (colorNo !== null) { + tr_class = 'color'+colorNo; + } + if (commit.boundary) { + tr_class += ' boundary'; + } + if (commit.nprevious === 0) { + tr_class += ' no-previous'; + } else if (commit.nprevious > 1) { + tr_class += ' multiple-previous'; + } + tr.className = tr_class; + + /* ? */ + if (i === 0) { + td_sha1.title = commit.info; + td_sha1.rowSpan = group.numlines; + + a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1; + if (a_sha1.firstChild) { + a_sha1.firstChild.data = commit.sha1.substr(0, 8); + } else { + a_sha1.appendChild( + document.createTextNode(commit.sha1.substr(0, 8))); + } + if (group.numlines >= 2) { + var fragment = document.createDocumentFragment(); + var br = document.createElement("br"); + var match = commit.author.match(/\b([A-Z])\B/g); + if (match) { + var text = document.createTextNode( + match.join('')); + } + if (br && text) { + var elem = fragment || td_sha1; + elem.appendChild(br); + elem.appendChild(text); + if (fragment) { + td_sha1.appendChild(fragment); + } + } + } + } else { + //tr.removeChild(td_sha1); // DOM2 Core way + tr.deleteCell(0); // DOM2 HTML way + } + + /* 123 */ + var linenr_commit = + ('previous' in commit ? commit.previous : commit.sha1); + var linenr_filename = + ('file_parent' in commit ? commit.file_parent : commit.filename); + a_linenr.href = projectUrl + 'a=blame_incremental' + + ';hb=' + linenr_commit + + ';f=' + encodeURIComponent(linenr_filename) + + '#l' + (group.srcline + i); + + blamedLines++; + + //updateProgressInfo(); + } +} + +// ---------------------------------------------------------------------- + +/**#@+ + * @constant + */ +var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/; +var infoRe = /^([a-z-]+) ?(.*)/; +var endRe = /^END ?([^ ]*) ?(.*)/; +/**@-*/ + +var curCommit = new Commit(); +var curGroup = {}; + +/** + * Parse output from 'git blame --incremental [...]', received via + * XMLHttpRequest from server (blamedataUrl), and call handleLine + * (which updates page) as soon as blame entry is completed. + * + * @param {String[]} lines: new complete lines from blamedata server + * + * @globals commits, curCommit, curGroup, t_interval_server, cmds_server + * @globals sha1Re, infoRe, endRe + */ +function processBlameLines(lines) { + var match; + + for (var i = 0, len = lines.length; i < len; i++) { + + if ((match = sha1Re.exec(lines[i]))) { + var sha1 = match[1]; + var srcline = parseInt(match[2], 10); + var resline = parseInt(match[3], 10); + var numlines = parseInt(match[4], 10); + + var c = commits[sha1]; + if (!c) { + c = new Commit(sha1); + commits[sha1] = c; + } + curCommit = c; + + curGroup.srcline = srcline; + curGroup.resline = resline; + curGroup.numlines = numlines; + + } else if ((match = infoRe.exec(lines[i]))) { + var info = match[1]; + var data = match[2]; + switch (info) { + case 'filename': + curCommit.filename = unquote(data); + // 'filename' information terminates the entry + handleLine(curCommit, curGroup); + updateProgressInfo(); + break; + case 'author': + curCommit.author = data; + break; + case 'author-time': + curCommit.authorTime = parseInt(data, 10); + break; + case 'author-tz': + curCommit.authorTimezone = data; + break; + case 'previous': + curCommit.nprevious++; + // store only first 'previous' header + if (!('previous' in curCommit)) { + var parts = data.split(' ', 2); + curCommit.previous = parts[0]; + curCommit.file_parent = unquote(parts[1]); + } + break; + case 'boundary': + curCommit.boundary = true; + break; + } // end switch + + } else if ((match = endRe.exec(lines[i]))) { + t_interval_server = match[1]; + cmds_server = match[2]; + + } else if (lines[i] !== '') { + // malformed line + + } // end if (match) + + } // end for (lines) +} + +/** + * Process new data and return pointer to end of processed part + * + * @param {String} unprocessed: new data (from nextReadPos) + * @param {Number} nextReadPos: end of last processed data + * @return {Number} end of processed data (new value for nextReadPos) + */ +function processData(unprocessed, nextReadPos) { + var lastLineEnd = unprocessed.lastIndexOf('\n'); + if (lastLineEnd !== -1) { + var lines = unprocessed.substring(0, lastLineEnd).split('\n'); + nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */; + + processBlameLines(lines); + } // end if + + return nextReadPos; +} + +/** + * Handle XMLHttpRequest errors + * + * @param {XMLHttpRequest} xhr: XMLHttpRequest object + * @param {Number} [xhr.pollTimer] ID of the timeout to clear + * + * @globals commits + */ +function handleError(xhr) { + errorInfo('Server error: ' + + xhr.status + ' - ' + (xhr.statusText || 'Error contacting server')); + + if (typeof xhr.pollTimer === "number") { + clearTimeout(xhr.pollTimer); + delete xhr.pollTimer; + } + commits = {}; // free memory +} + +/** + * Called after XMLHttpRequest finishes (loads) + * + * @param {XMLHttpRequest} xhr: XMLHttpRequest object + * @param {Number} [xhr.pollTimer] ID of the timeout to clear + * + * @globals commits + */ +function responseLoaded(xhr) { + if (typeof xhr.pollTimer === "number") { + clearTimeout(xhr.pollTimer); + delete xhr.pollTimer; + } + + fixColorsAndGroups(); + writeTimeInterval(); + commits = {}; // free memory +} + +/** + * handler for XMLHttpRequest onreadystatechange event + * @see startBlame + * + * @param {XMLHttpRequest} xhr: XMLHttpRequest object + * @param {Number} xhr.prevDataLength: previous value of xhr.responseText.length + * @param {Number} xhr.nextReadPos: start of unread part of xhr.responseText + * @param {Number} [xhr.pollTimer] ID of the timeout (to reset or cancel) + * @param {Boolean} fromTimer: if handler was called from timer + */ +function handleResponse(xhr, fromTimer) { + + /* + * xhr.readyState + * + * Value Constant (W3C) Description + * ------------------------------------------------------------------- + * 0 UNSENT open() has not been called yet. + * 1 OPENED send() has not been called yet. + * 2 HEADERS_RECEIVED send() has been called, and headers + * and status are available. + * 3 LOADING Downloading; responseText holds partial data. + * 4 DONE The operation is complete. + */ + + if (xhr.readyState !== 4 && xhr.readyState !== 3) { + return; + } + + // the server returned error + // try ... catch block is to work around bug in IE8 + try { + if (xhr.readyState === 3 && xhr.status !== 200) { + return; + } + } catch (e) { + return; + } + if (xhr.readyState === 4 && xhr.status !== 200) { + handleError(xhr); + return; + } + + // In konqueror xhr.responseText is sometimes null here... + if (xhr.responseText === null) { + return; + } + + + // extract new whole (complete) lines, and process them + if (xhr.prevDataLength !== xhr.responseText.length) { + xhr.prevDataLength = xhr.responseText.length; + var unprocessed = xhr.responseText.substring(xhr.nextReadPos); + xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos); + } + + // did we finish work? + if (xhr.readyState === 4) { + responseLoaded(xhr); + return; + } + + // if we get from timer, we have to restart it + // otherwise onreadystatechange gives us partial response, timer not needed + if (fromTimer) { + setTimeout(function () { + handleResponse(xhr, true); + }, 1000); + + } else if (typeof xhr.pollTimer === "number") { + clearTimeout(xhr.pollTimer); + delete xhr.pollTimer; + } +} + +// ============================================================ +// ------------------------------------------------------------ + +/** + * Incrementally update line data in blame_incremental view in gitweb. + * + * @param {String} blamedataUrl: URL to server script generating blame data. + * @param {String} bUrl: partial URL to project, used to generate links. + * + * Called from 'blame_incremental' view after loading table with + * file contents, a base for blame view. + * + * @globals t0, projectUrl, div_progress_bar, totalLines +*/ +function startBlame(blamedataUrl, bUrl) { + + var xhr = createRequestObject(); + if (!xhr) { + errorInfo('ERROR: XMLHttpRequest not supported'); + return; + } + + t0 = new Date(); + projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';'); + if ((div_progress_bar = document.getElementById('progress_bar'))) { + //div_progress_bar.setAttribute('style', 'width: 100%;'); + div_progress_bar.style.cssText = 'width: 100%;'; + } + totalLines = countLines(); + updateProgressInfo(); + + /* add extra properties to xhr object to help processing response */ + xhr.prevDataLength = -1; // used to detect if we have new data + xhr.nextReadPos = 0; // where unread part of response starts + + xhr.onreadystatechange = function () { + handleResponse(xhr, false); + }; + + xhr.open('GET', blamedataUrl); + xhr.setRequestHeader('Accept', 'text/plain'); + xhr.send(null); + + // not all browsers call onreadystatechange event on each server flush + // poll response using timer every second to handle this issue + xhr.pollTimer = setTimeout(function () { + handleResponse(xhr, true); + }, 1000); +} + +/* end of blame_incremental.js */ diff --git a/Git-server/Stuff/coffee-server.html b/Git-server/Stuff/coffee-server.html new file mode 100644 index 0000000..16a37a7 --- /dev/null +++ b/Git-server/Stuff/coffee-server.html @@ -0,0 +1,172 @@ + + + + + + + + Coffee server + + + + + + + + + +
+
+
+ a red coffee pot +
+
+

Coffees served today:

+
+

+ This server serves coffees as proposed in famous + RFC 2324. + If you want one, simply send a HTTP (or HTTPS) request with BREW method using curl. +

+
+ curl -X BREW http://git.dpolakovic.space +
+

+ Your coffee will be brewed in one of my two coffee pots. Red and white one. + You can also pick coffee pot by name, while typing the the URL. + You can use their canonical names from the RFC (pot-0 and pot-1) or their + verbose variants (red-coffee-pot and white-coffee-pot). [*] +

+

+ Before specifying the pot in BREW request, make sure it's not being cleaned or empty + by making a request with GET method. +

+
+ curl -X GET http://git.dpolakovic.space/white-coffee-pot +
+

+ If your chosen coffee pot is empty, you have to wait until the server + refills the coffee beans or you can refill them yourself with POST method. +

+
+ curl -X POST http://git.dpolakovic.space/white-coffee-pot +
+

+ If you preffer coffee with some additions, you can include them in + your request. If they are available, the server will happily prepare + your order. +

+
+ + # Coffee with cream + + curl -X BREW \ + -H "Content-Type: application/coffee-pot-command" \ + -H "Accept-Additions: Cream" \ + http://git.dpolakovic.space/ + + + + # Coffee with chocolate and cream + + curl -X BREW \ + -H "Content-Type: application/coffee-pot-command" \ + -H "Accept-Additions: Chocolate, Cream" \ + http://git.dpolakovic.space/ + + + + # Or as substitution for WHEN method (q values = quantity in ml) + + curl -X BREW \ + -H "Content-Type: application/coffee-pot-command" \ + -H "Accept-Additions: Whole-Milk;q=50" \ + http://git.dpolakovic.space/ + +
+

+ As seen in the last example, the WHEN method is substitued by quantity ordering. + You don't have to hold the connection open until your milk is poured. Simply + declare how much milk (or any other addition) you would like to have in your coffee. +

+

+ This server offers updated list of additions, to fit modern coffee trends. + The original RFC didn't specified plant based milk types like pea milk or + other addition types like + nowadays very popular pumpkin puree or cinnamon. + Complete offer of additions and coffee pots can be found with PROPFIND method. +

+
+ curl -X PROPFIND http://git.dpolakovic.space +
+

+ If you are not currenttly on Unix or Unix-like system, you can request your coffee + using telnet. +

+
+ telnet git.dpolakovic.space 80 + BREW / HTTP/1.0 + Host: git.dpolakovic.space +
+

+ I even made a perl script which offers + simplified way to request coffee from any machine. You can find it right + here on this git server as + htcpcp-requestor. +

+

+ The counter you see stores BREW method request from past 48 hours and list + all from midnight of your timezone. Link to the source code is available + also + here + on this git server. +

+

+ I was also thinking about implementing the + Coke protocol, + but I don't own any soda vending machine. If you have one and you + are willing to donate it for the good of the Internet, please send me + an email. +

+
+
+

+ * Quick tought about coffee pot numbering
+ The RFC 2324 defines the canonical pot name in format "pot-[integer]". This + is confusing as there is no first known member of integer group. + There is however first known member of integer data type which is 0, but the + mathematical first member of the type could also be -2,147,483,648. + On the other hand, most natural number asigned to first member of any group + (even group of coffee pots) which is also member of integer group is 1. + Therefore, + the red coffee pot has working aliases: pot-2 (since the pot-0 and pot-1 + redirecting to same pot could evoke bug behaviour), pot--2147483648 and + pot-n2147483648. + The white coffee pot is then pot-1, even tough it is not the default option. + To avoid any possible confusion from numbering coffee pots, I also implemented + their human readable color based names, which is present throughout this guide. +
+

+
+ +
+ Copyright 2022-2026 David Polakovic.
+ This page uses trivial JavaScript. This page doesn't use cookies. Source code available + here + under + GPLv3 license. +
+ + + + \ No newline at end of file diff --git a/Git-server/Stuff/contributors-guide.html b/Git-server/Stuff/contributors-guide.html new file mode 100644 index 0000000..e19ab7b --- /dev/null +++ b/Git-server/Stuff/contributors-guide.html @@ -0,0 +1,209 @@ + + + + + + + + Contributor's guide + + + + + + + + + +
+

+ This guide will show you everything you need to do, in order to contribute a project. + It is primarily oriented towards users of Unix or Unix-like operating systems, + but I tested it on Windows running Git bash and it worked just fine. +

+
    +
  1. SSH setup
  2. +
  3. Git setup
  4. +
  5. Contributing to a project
  6. +
  7. FAQ
  8. +
+
+ + +

SSH setup

+

+ Contributors perform clone and push to the server using ssh key. + The key has to be ed25519 type in RFC 4716 format. You can generate one with this + command: +

+
+ ssh-keygen -t ed25519_git_dpolakovic_space -C "your.email@example.com" +
+

+ Use email address which you will add later in your git config. +

+

+ + Privacy consideration: this email address will be visible on web in log + and shortlog. + +

+

+ Use long passphrase and store your keys securely. If you use older ssh package, + you might end up with key in Open SSH format. In that case, convert the public key into + desired format with command: +

+
+ ssh-keygen -e -f ~/.ssh/ed25519_git_dpolakovic_space.pub -m RFC4716 +
+

+ Copy the newly generated/converted public ssh key contents into an email. With it, + name projects you want to contribute to, so I can add you correct access rights. + As soon as I reply to your email, you can start contributing. +

+

+ In meantime, you can modify your ssh config file. +

+
+ nano ~/.ssh/config +
+

+ + Note: you can actually use any text editor you preffer, I put nano + here because it is default editor in Windows Git Bash and it's + favorite editor of my good friend Jakub. + +

+

+ Add these lines: +

+

+

+Host git.dpolakovic.space + HostName git.dpolakovic.space + User git + Port 1388 + IdentityFile ~/.ssh/ed25519_git_dpolakovic_space + IdentitiesOnly yes +
+

+
+

Git setup

+

+ If you already obtained the confirmation reply, you can clone projects + using this command structure: +

+
+ git clone git@git.dpolakovic.space:<project-name> +
+

+ You can test your setup on testing repository: +

+
+ git clone git@git.dpolakovic.space:testing +
+

+ Enter the directory and create your branch: +

+
+ git checkout -b "my-new-branch" +
+

+ If you haven't setup your global git config name and email, it's + good time to set it up now. Please use the email you entered + for ssh-keygen before. +

+
+ git config user.name --global "Your Name" + git config user.email --global "your.email@example.com" +
+

+ Or set it up for project only +

+

+

+ git config user.name "Your Name" + git config user.email "your.email@example.com" +
+

+
+

Contributing to a project

+

+ When you created some changes, perform common git workflow: +

+
+ # inlcude your changes + git add * + + # create commit(s) + git commit -m "docs: your commit message" + + # push your changes + git push origin my-new-branch +
+

+ You will be prompted to enter your ssh key passphrase. Submit it and + your changes should be there. Confirm it by visiting the + testing repository. +

+

+ + Note: if your email has gravatar account, your gravatar profile picture + will be visible on this server too. + +

+

+ That's it! For more git commands reffer to this + cheat sheet + or + git documentation. +

+ +
+

FAQ

+

+ Q: How can I leave the server?
+ A: Just send me an email with this request and I will delete your ssh key. +

+

+ Q: Can I become a collaborator?
+ A: Sadly no. +

+

+ Q: Could you mirror a project on different git server?
+ A: Yes. I preffer codeberg or notabug.org, but I am willing to mirror + on other places too, if you wish so. +

+

+ Q: Can I host my own project on this server?
+ A: Well, generally no, but it depends on the project. Since it's + unusual request, send me an email. +

+ +
+ +
+ Copyright 2022-2026 David Polakovic.
+ This page uses trivial JavaScript. This page doesn't use cookies. Source code available + here + under + GPLv3 license. +
+ Server for this subdomain is + RFC 2324 + compliant +
+ + + + \ No newline at end of file diff --git a/Git-server/Stuff/dot.png b/Git-server/Stuff/dot.png new file mode 100644 index 0000000..c4c7217 Binary files /dev/null and b/Git-server/Stuff/dot.png differ diff --git a/Git-server/Stuff/server-rules.html b/Git-server/Stuff/server-rules.html new file mode 100644 index 0000000..0b6a6f6 --- /dev/null +++ b/Git-server/Stuff/server-rules.html @@ -0,0 +1,111 @@ + + + + + + + + Server rules + + + + + + + + + +
+

+ These rules are expected to be followed by contributors on this server. + It's a mix of some good practices, my preferences and server setup. I + hope you won't find them very restrictive as I sincerely welcome everyone + who wants to help me. +

+
+ + +
    +
  1. + A contributor may participate in up to two projetcs at once, + excluding the testing project. +
  2. +
  3. + Every commit must include a type and subject. Type must be one of: + feat, docs, chore, style, or perf. Subject should be + concise and descriptive. +
  4. +
  5. + Larger commits must include changes in bullet points. + Each point should describe a distinct change. +
  6. +
  7. + Always check if your changes don't colide with current project license. +
  8. +
  9. + No contributor is allowed to push project license changes. +
  10. +
  11. + Resolve potential conflicts before asking for merge. +
  12. +
  13. + When contributing to project from category "2 - scripts", try to avoid + using external libraries. +
  14. +
  15. + Don't change structure declared in README.txt, especially + in projects from category "1 - programs". +
  16. +
  17. + It's strictly forbidden to change README.txt file to README.md. +
  18. +
  19. + Comment the code. Use signle line comment describing what it does. + If necessary, include second line about how it's done and/or third line + explaining weird choices you decided to implement. +
  20. +
  21. + Try to avoid lines longer than 90 characters. +
  22. +
  23. + No editor-specific + pushes are allowed (like incluson of #files generated by Emacs). +
  24. +
  25. + In project related (or any) email communication use inline replies. +
  26. +
+
+

+ +
+ +
+ Copyright 2022-2026 David Polakovic.
+ This page uses trivial JavaScript. This page doesn't use cookies. Source code available + here + under + GPLv3 license. +
+ Server for this subdomain is + RFC 2324 + compliant +
+ + + + \ No newline at end of file diff --git a/Git-server/Stuff/stuff.css b/Git-server/Stuff/stuff.css new file mode 100644 index 0000000..1e5b57c --- /dev/null +++ b/Git-server/Stuff/stuff.css @@ -0,0 +1,121 @@ +body { + font-family: sans-serif; + font-size: 14px; + border: solid #d9d8d1; + border-width: 1px; + /* margin: 10px; */ + + width:1000px; + margin: 0 auto; + margin-bottom: 20px; + background-color: #ffffff; + color: #000000; + padding-left: 20px; + padding-right: 20px; +} + +main { + padding-left: 13px; + padding-top: 10px; + width: 790px; +} + +a { + color: #0000cc; +} + +a:hover, a:visited, a:active { + color: #551a8b; +} + +a:hover { + color: #4682b4; +} + +html { + background-image: url("https://dpolakovic.space/Pictures/sky.jpg") +} + +div.page_header a:visited, a.header { + color: #0000cc; +} + +.banner { + width: 100%; + margin: 0 auto; + max-width: 1000px; + padding-left: 2px; + padding-top: 3px; +} + +.banner img { + max-width: 100%; +} + +.banner a { + text-decoration: none; + +} + +div.page_header { + height: 25px; + padding: 8px; + font-size: 150%; + font-weight: bold; + background-color: rgb(217, 216, 209); +} + +div.page_footer { + height: 22px; + padding: 4px 8px; + background-color: #d9d8d1; +} + +div.page_footer_text { + line-height: 22px; + float: left; + color: #555555; + font-style: italic; +} + +.center { + padding-left: 380px; +} + +footer { + padding-top: 15px; + padding-bottom: 20px; + text-align: center; + font-size: small; +} + +code { + display: inline; + padding: 2px 6px; + margin: 0; + font-family: 'Courier New', Consolas, monospace; + background-color: #f4f4f4; + border: 1px solid #ddd; + border-radius: 3px; + font-size: 0.9em; + color: #333; + white-space: nowrap; + padding-left: 15px; + padding-right: 15px; +} + +.code-block { + display: block; + width: 750px; + padding: 2px 6px; + margin: 0; + font-family: 'Courier New', Consolas, monospace; + background-color: #f4f4f4; + border: 1px solid #ddd; + border-radius: 3px; + font-size: 0.9em; + color: #333; + white-space: pre-wrap; + padding-left: 15px; + padding-right: 15px; +} diff --git a/Git-server/Stuff/stuff.js b/Git-server/Stuff/stuff.js new file mode 100644 index 0000000..41ab88d --- /dev/null +++ b/Git-server/Stuff/stuff.js @@ -0,0 +1,69 @@ +/** + * + * @licstart The following is the entire license notice for the + * JavaScript code in this page. + * + * Copyright (C) 2025 David Polakovic + * + * + * The JavaScript code in this page is free software: you can + * redistribute it and/or modify it under the terms of the GNU + * General Public License (GNU GPL) as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) + * any later version. The code is distributed WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. + * + * As additional permission under GNU GPL version 3 section 7, you + * may distribute non-source (e.g., minimized or compacted) forms of + * that code without the copy of the GNU GPL normally required by + * section 4, provided you include this license notice and a URL + * through which recipients can access the Corresponding Source. + * + * @licend The above is the entire license notice + * for the JavaScript code in this page. + * +*/ + +const dotImg = document.getElementById("toggleImageDot"); +const spaceImg = document.getElementById("toggleImageSpace"); +const dpoImg = document.getElementById("toggleImagedpo"); + +// Mouse behavior for dot +if (dotImg) { + dotImg.addEventListener("mousedown", () => { + dotImg.src = "https://dpolakovic.space/Pictures/dot2.png"; + }); + dotImg.addEventListener("mouseup", () => { + dotImg.src = "https://dpolakovic.space/Pictures/dot.png"; + }); + dotImg.addEventListener("mouseleave", () => { + dotImg.src = "https://dpolakovic.space/Pictures/dot.png"; + }); +} + +// Mouse behavior for space +if (spaceImg) { + spaceImg.addEventListener("mousedown", () => { + spaceImg.src = "https://dpolakovic.space/Pictures/space2.png"; + }); + spaceImg.addEventListener("mouseup", () => { + spaceImg.src = "https://dpolakovic.space/Pictures/space.png"; + }); + spaceImg.addEventListener("mouseleave", () => { + spaceImg.src = "https://dpolakovic.space/Pictures/space.png"; + }); +} + +// Mouse behavior for dpo +if (dpoImg) { + dpoImg.addEventListener("mousedown", () => { + dpoImg.src = "https://dpolakovic.space/Pictures/dpo2.png"; + }); + dpoImg.addEventListener("mouseup", () => { + dpoImg.src = "https://dpolakovic.space/Pictures/dpolakovic.png"; + }); + dpoImg.addEventListener("mouseleave", () => { + dpoImg.src = "https://dpolakovic.space/Pictures/dpolakovic.png"; + }); +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..36d2e89 --- /dev/null +++ b/README.txt @@ -0,0 +1,158 @@ +================================================================================ + dpolakovic.space + website files for dpolakovic.space and all it's subdomains +================================================================================ + +AUTHOR : David Polakovic +EMAIL : email@dpolakovic.space +TXT VERSION : 1.0.0 +LAST UPDATED: 2026-02-24 +LICENSE : GPLv3, with exception of /Git-server/Gtiweb folder + +-------------------------------------------------------------------------------- +TABLE OF CONTENTS +-------------------------------------------------------------------------------- + + 1. Overview + 2. Project structure + 3. Requirements + 4. Installation + 5. Usage + 6. Reference + 7. Configuration + 8. Examples + 9. Troubleshooting + 10. License + +-------------------------------------------------------------------------------- +1. OVERVIEW +-------------------------------------------------------------------------------- + + This file describes everything I host on my server. + +-------------------------------------------------------------------------------- +2. PROJECT STRUCTURE +-------------------------------------------------------------------------------- + + dpolakovic-space + ├── Git-server ## files from git.dpolakovic.space + │   ├── Coffee ## RFC 2324 implementation + │   │   ├── coffee-counter.js # counts served coffees from 48h ago + │   │   └── coffee-server.php # custom http methods definitions + │   ├── Gitweb ## gitweb (front page) files + │   │   ├── static + │   │   │   ├── gitweb.css + │   │   │   └── gitweb.js + │   │   ├── banner.html # contains JS for clicky images + │   │   ├── footer.html # contains JS which counts coffees + │   │   ├── gitweb.cgi # front page logic customization + │   │   └── indextext.html # contains styles for front page + │   └── Stuff ## other pages present on git server + │   ├── coffee-server.html + │   ├── contributors-guide.html + │   ├── dot.png + │   ├── server-rules.html + │   ├── stuff.css + │   └── stuff.js + │ + ├── Root-domain ## files from www.dpolakovic.space + │   ├── blogs ## contains all blog pages + │   ├── error + │   │   └── error.php # custom http status code handler + │   ├── php + │   │   ├── config.php # logic file included in all pages + │   │   └── leaderboard.php # logic file for leaderboard of CTF + │   ├── Pictures + │   ├── Styles + │   │   ├── FONT_LICENSE.txt + │   │   ├── NotoSerif-Bold.ttf + │   │   ├── NotoSerif-Italic.ttf + │   │   ├── NotoSerif-Regular.ttf + │   │   └── styles.css + │   ├── brew.php # implementation of RFC 2324 + │   ├── clicky-images.js + │   └── example-page.php + │ + ├── Tor-site ## files from Tor site + │   ├── Dead-drops ## encrypted messages from dead drop + │   ├── php + │   │   ├── captcha.php # captcha for dead drops + │   │   ├── config.php # config for all pages + │   │   └── dd2.php # dead drop logic + │   ├── Pictures + │   ├── Styles + │   │   ├── FONT_LICENSE.txt + │   │   ├── NotoSerif-Bold.ttf + │   │   ├── NotoSerif-Italic.ttf + │   │   ├── NotoSerif-Regular.ttf + │   │   └── styles.css + │   ├── dead-drop.php # dead drop front page + │   └── example-page.php + │ + ├── LICENSE.txt + └── README.txt + + +-------------------------------------------------------------------------------- +3. REQUIREMENTS +-------------------------------------------------------------------------------- + + - web server (tested on Apache) + - PHP (any version) + - git (any version < 3.0.0) + - gitweb (any version) + +-------------------------------------------------------------------------------- +4. INSTALLATION +-------------------------------------------------------------------------------- + + 1. Clone or download this repository. + + 2. Copy any file you want into your server. + +-------------------------------------------------------------------------------- +5. USAGE +-------------------------------------------------------------------------------- + + - + +-------------------------------------------------------------------------------- +6. REFERENCE +-------------------------------------------------------------------------------- + + Short references are written in project structure. + +-------------------------------------------------------------------------------- +7. CONFIGURATION +-------------------------------------------------------------------------------- + + - + +-------------------------------------------------------------------------------- +8. EXAMPLES +-------------------------------------------------------------------------------- + + - + +-------------------------------------------------------------------------------- +9. TROUBLESHOOTING +-------------------------------------------------------------------------------- + + Problem : Coffee counter shows error. + Solution: Restart the web server. This generates new json files and usually + solves this + + Problem : Dead drops don't show contents. + Solution: Check permissions and ownership on Dead-drops/ folder. It should + be apache:apache + +-------------------------------------------------------------------------------- +10. LICENSE +-------------------------------------------------------------------------------- + + This project is licensed under the GPLv3 License. + See the LICENSE.txt file for full details. + + Copyright (c) 2026 David Polakovic + +================================================================================ diff --git a/Root-domain/Styles/FONT_LICENSE.txt b/Root-domain/Styles/FONT_LICENSE.txt new file mode 100644 index 0000000..6843f31 --- /dev/null +++ b/Root-domain/Styles/FONT_LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Root-domain/Styles/NotoSerif-Bold.ttf b/Root-domain/Styles/NotoSerif-Bold.ttf new file mode 100755 index 0000000..aa21d44 Binary files /dev/null and b/Root-domain/Styles/NotoSerif-Bold.ttf differ diff --git a/Root-domain/Styles/NotoSerif-Italic.ttf b/Root-domain/Styles/NotoSerif-Italic.ttf new file mode 100755 index 0000000..786ecc7 Binary files /dev/null and b/Root-domain/Styles/NotoSerif-Italic.ttf differ diff --git a/Root-domain/Styles/NotoSerif-Regular.ttf b/Root-domain/Styles/NotoSerif-Regular.ttf new file mode 100755 index 0000000..dc30297 Binary files /dev/null and b/Root-domain/Styles/NotoSerif-Regular.ttf differ diff --git a/Root-domain/Styles/styles.css b/Root-domain/Styles/styles.css new file mode 100755 index 0000000..fa30641 --- /dev/null +++ b/Root-domain/Styles/styles.css @@ -0,0 +1,776 @@ +/* Styles.css + this is a file for setting visual styles of the web page + I am no webdesigner so the comments are rather simple + copyright 2023, David Polakovic, licensed under GPLv3, see README +*/ + + +/* set both background and font color to keep content readable for + users with different text colors in browser settings. +*/ + + +/* I love this font. */ +@font-face { + font-family: 'Noto Serif'; + src: url('/Styles/NotoSerif-Regular.ttf') format('truetype'); + font-weight: 450; + font-style: normal; +} + +@font-face { + font-family: 'Noto Serif'; + src: url('/Styles/NotoSerif-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Noto Serif'; + src: url('/Styles/NotoSerif-Italic.ttf') format('truetype'); + font-weight: normal; + font-style: italic; +} + +body { + background-color: #E8CBA9; + color: #000000; + font-family: 'Noto Serif', serif; /* Set font to Noto Serif */ + font-size: 18px; +} + +table { + border-collapse: collapse; + border: 1px solid grey; + width: 100%; +} + +th { + background-color: #c69879; + border: 1px solid grey; +} + +td { + padding: 2px; + border: 1px solid grey; +} + +tr:hover { + background-color: #efdac3; +} + +.center { + text-align: center; +} + +.my-filter { + text-align: center; + padding-bottom: 50px; +} + +/* Navigation bar + just simple bar from W3 schools with minor edits + https://www.w3schools.com/Css/css_navbar_horizontal.asp +*/ + +nav { + display: flex; +} +.nav-bar > ul { +list-style-type: none; +max-width: 1000px; +width: 100%; +margin: 0 auto; +padding: 0; +background-color: #333333; +border-radius: 10px; +} + +.nav-bar li { +float: left; +position: relative; /* Added for dropdown positioning */ +} +.nav-bar li a { +display: block; +color: white; +text-align: center; +padding: 14px 18px; +text-decoration: none; +} +.nav-bar li:last-child { +float: right; +position: relative; +} +.nav-bar li:last-child a { +border-radius: 0 14px 14px 0; +} +.gitserver { +background-color: #1e1e1e; +} +.gitstatus { +display: none; +list-style-type: none; +position: absolute; +color: black; +} +.nav-bar li:last-child:hover .gitstatus { +display: block; +} + +/* Dropdown styles */ +.dropdown { +display: block; +color: white; +text-align: center; +text-decoration: none; +cursor: pointer; +} + +/* Hide the checkbox */ +.dropdown input[type="checkbox"] { +display: none; +} + +/* Style the label like the dropdown text */ +.dropdown label { +display: block; +color: white; +text-align: center; +padding: 14px 16px; +text-decoration: none; +cursor: pointer; +} + +.dropdown-content { +display: none; +position: absolute; +background-color: #333333; +min-width: 160px; +z-index: 1; +top: 100%; +left: 0; +border-radius: 0 0 10px 10px; +list-style-type: none; +padding: 0; +} + +.dropdown-content li { +float: none; +width: 100%; +} + +.dropdown-content a { +color: white; +padding: 12px 16px; +text-decoration: none; +display: block; +text-align: left; +} +.dropdown-content a:hover { +} +.dropdown-content a:last-child { +border-radius: 0 0 10px 10px; +} + +/* Show dropdown when checkbox is checked */ +.dropdown input[type="checkbox"]:checked ~ .dropdown-content { +display: block; +} + +/* Keep the hover functionality for desktop */ +.dropdown:hover .dropdown-content { +display: block; +} +.dropdown:hover { +} + +/* +.gitstatus li::before { + content: ''; + width: 10px; + height: 10px; + border-radius: 50%; + position: absolute; + top: 50%; + left: -14px; + transform: translateY(-50%); + background-color: green; +} + +.gitstatus.offline li:before { + background-color: red; +} +*/ + +/* Mobile burger menu styles */ +.mobile-menu-toggle { + display: none; +} + +.mobile-menu-icon { + display: none; + cursor: pointer; + padding: 14px 16px; + color: white; + font-size: 24px; +} + +/* Banner pictures + style pictures shown on top of every page - should be all same size, + but just in case +*/ +.banner { + width: 100%; + margin: 0 auto; + max-width: 1000px; +} + +.banner img { + max-width: 100%; +} + +/* when pictures are made into links, they show little dash in right corner + of the picture - this deletes that +*/ +.banner a { + text-decoration: none; + +} + +/* Main content + style for main content of each page - centers text and other stuff +*/ +.content { + max-width: 975px; + height: auto; + width: 100%; + margin: 0 auto; + text-align: justify; + line-height: 1.25; +} + +.less { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.content img { + max-width: 100%; +} + +.content img.bee-gifs { + max-width: 105px; +} + +.hide-link { + color: inherit; + text-decoration: inherit; +} + +/* Footer + Simple footer inspired by navigation bar - changed text to be white +*/ +.footer { + max-width: 980px; + width: auto; + margin: 0 auto; + margin-bottom: 2em; + padding: 10px; + text-align: center; + background-color: #b38265; + font-size: 13px; + color: white; + border-radius: 10px; + +} + +/* changes colors of links to white in footer */ +.footer a { + color: white; +} + +/* this is class for my public GPG key fetch - only used once */ +.text-area { + font-family: monospace; +} + +/* this is class for anybrowser button to appear far right */ +.gif-buttons { + float: right; + line-height: 1; + margin-left: 5px; /* Add 10px space to the left of each button */s +} + +.dir { + margin-left: 10px; +} + +/* Blog previews + these are clases for my blog page - they show small picture and + description in small fonts +*/ +.blog-prev { + display: flex; +} + +.blog-prev-img { + width: 150px; + height: 95px; + float: left; +} + +.blog-prev-img img { + width: 150px; + height: 95px; + object-fit: cover; + +} + +.blog-prev-text { + margin-left: 10px; + height: 95px; + float: left; + color: #545454; +} + +.fresh-link:visited { + color: rgb(0, 0, 238); +} + +.filter:link, +.filter:visited { + color: rgb(0, 0, 238); +} + +.filter.active { + color: red; +} + +/* Navigation bar for blog page + So here is modified nav bar with only one option (Go back) + I put it as a secound style, because I don't want last child + to be pushed on the far right side (as it was for the "Git server" +*/ + +.nav-bar-blog {} + +.nav-bar-blog ul { + list-style-type: none; + max-width:1000px; + width: 100%; + margin: 0 auto; + padding: 0; + overflow: hidden; + background-color: #333; + border-radius: 10px; +} + +/* make everything stick to left side */ +.nav-bar-blog li { + float: left; +} + +/* style for contents of the nav bar */ +.nav-bar-blog li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + + +/* Block of code + This is what I use for block of code formating +*/ + +.code-block { + background: #efdac3; + border: 1px solid #ddd; + border-left: 3px solid #b38265; + color: #666; + page-break-inside: avoid; + font-family: monospace; + font-size: 15px; + line-height: 1.6; + margin-bottom: 1.6em; + max-width: 100%; + overflow-x: auto; /* Enable horizontal scrolling */ + white-space: nowrap; /* Prevent text wrapping */ + padding: 1em 1.5em; + display: block; + word-wrap: break-word; +} + +.code-block2 { + background: #efdac3; + border: 1px solid #ddd; + border-left: 3px solid #b38265; + color: #666; + page-break-inside: avoid; + font-family: monospace; + font-size: 15px; + line-height: 1.6; + margin-bottom: 1.6em; + max-width: 100%; + overflow-x: auto; /* Enable horizontal scrolling */ + white-space: nowrap; /* Prevent text wrapping */ + padding: 1em 1.5em; + display: block; + word-wrap: break-word; + white-space: pre-wrap; +} + + + +.terminal-block { + background: #000; + border: 1px solid #000; + /*border-left: 3px solid #f36d33; */ + border-left: 3px solid #000; + color: white; + /* color: #99ffe6; + color: #80ffdf; */ + font-weight: bold; + page-break-inside: avoid; + font-family: monospace; + font-size: 15px; + line-height: 1.6; + margin-bottom: 1.6em; + max-width: 100%; + overflow-x: auto; /* Enable horizontal scrolling */ + white-space: nowrap; /* Prevent text wrapping */ + padding: 1em 1.5em; + display: block; + word-wrap: break-word; + white-space: pre-wrap; +} + +.correspondence-block { + background: #f5f5f0; + border: 1px solid #ddd; + border-left: 3px solid #4a90e2; + color: #333; + page-break-inside: avoid; + font-family: 'Times New Roman', Times, serif; + font-size: 18px; + line-height: 1.3; + margin-bottom: 1em; + max-width: 100%; + overflow-x: auto; + padding: 0.5em 1.5em; + display: block; + word-wrap: break-word; + white-space: pre-wrap; +} + +.correspondence-block .header { + border-bottom: 1px solid #999; + /*padding-bottom: 0.5em;*/ + margin-bottom: 0.75em; + margin-left: 1.9em; + font-weight: normal; + width: 600px; + max-width: 100%; +} + +.what-is-this { + max-width: 975px; + height: auto; + width: 100%; + margin: 0 auto; + text-align: justify; + color: #e8cba9; +} + +.what-is-this a { + color: inherit; + text-decoration: inherit; +} + +.error-block { + color: #973232; + width: 630px; + margin: 0 auto; +} + +.error-block a { + color: #c53333; +} + +.desktop-only { + display: block; /* Visible on desktop */ +} + +/* ============================================ + MOBILE RESPONSIVE STYLES + ============================================ + everything from down here is AI mumbo jumbo + because I struggle to find anything more + dumb than the resposive css. I hate doing this. + I did it after 3 years of having this web just + because lot o people were spamming my mailbox + that "the site looks ugly on their phone". + Well I did it, here u are, fuck you and fuck css. + */ + +@media screen and (max-width: 768px) { + + /* Prevent any horizontal scroll or overflow */ + html, body { + overflow-x: hidden; + width: 100%; + max-width: 100%; + margin: 0; + padding: 0; + font-size: 15px; + } + + * { + max-width: 100%; + box-sizing: border-box; + } + + /* Hide banner completely on mobile */ + .banner { + display: none; + } + + /* Mobile navigation - full width, no rounded corners, touching top */ + .mobile-menu-icon { + display: block; + background-color: #333333; + margin: 0; + text-align: left; + width: 100%; + border-radius: 0; + padding: 19px 10px; + } + + .mobile-menu-icon::after { + content: "▼ dpolakovic.space"; + font-size: 16px; + } + + nav { + display: block; + width: 100%; + margin-bottom: 2em; + } + + .nav-bar > ul { + display: none; + max-width: 100%; + width: 100%; + border-radius: 0; + margin: 0; + } + + .mobile-menu-toggle:checked ~ ul { + display: block; + } + + .nav-bar li { + float: none; + width: 100%; + border-bottom: 1px solid #444; + } + + .nav-bar li a { + text-align: left; + padding: 14px 15px; + padding-left: 30px; + border-radius: 0; + font-size: 16px; + } + + /* All ul/li text should be left-aligned on mobile */ + ul li { + text-align: left; + } + + /* Blog navigation bar - match the Menu label exactly with asymmetric padding */ + .nav-bar-blog { + margin: 0; + } + + .nav-bar-blog ul { + max-width: 100%; + width: 100%; + border-radius: 0; + margin: 0; + margin-bottom: 2em; + padding: 0; + list-style: none; + } + + .nav-bar-blog li { + float: none; + width: 100%; + margin: 0; + } + + .nav-bar-blog li a { + display: block; + background-color: #333333; + margin: 0; + text-align: left; + width: 100%; + border-radius: 0; + padding-top: 30px; + padding-bottom: 25px; + padding-left: 20px; + padding-right: 20px; + font-size: 16px; + color: white; + text-decoration: none; + line-height: 16px; + } + + .nav-bar li:last-child { + float: none; + border-bottom: none; + } + + .nav-bar li:last-child a { + border-radius: 0; + } + + /* Git server with dark background stays at bottom of menu */ + .gitserver { + background-color: #1e1e1e; + } + + /* Left-align text for blog posts */ + body.blog-post .content { + text-align: left; + } + + /* Glider button stays in content on mobile */ + .content .gif-buttons { + display: none; + } + + .content img.bee-gifs { + margin: 0 auto; + width: 75px; + height: 75px; + } + + /* Main content */ + .content { + max-width: 100%; + padding: 5px 12px; + margin: 0; + box-sizing: border-box; + } + + .content h2 { + margin-top: 0; + padding-left: 0; + } + + .content p { + padding-left: 0; + margin-left: 0; + } + + /* Center class - make links left-aligned and stacked on mobile, but keep images centered */ + .center { + text-align: left; + white-space: normal; + padding-left: 1.5em; /* Space for bullets */ + margin: 0; + } + + .center a { + display: list-item; + list-style-type: disc; /* Or 'circle', 'square', 'none' */ + margin-bottom: 0.5em; + } + + .center img { + display: block; + text-align: center; + margin: 0 auto; + } + + /* All links should be left-aligned on mobile */ + a { + text-align: left; + } + + /* Footer full width, edge to edge, stick to bottom */ + .footer { + max-width: 100%; + width: 100%; + border-radius: 0; + text-align: left; + padding: 15px 12px; + padding-bottom: 55px; + margin: 0; + position: relative; + } + + /* Hide what-is-this section on mobile */ + .what-is-this { + display: none; + } + + /* Error block responsive */ + .error-block { + width: 100%; + padding: 15px 20px; + } + + /* Blog previews stack on mobile */ + .blog-prev { + flex-direction: column; + } + + .blog-prev-img { + display: none; + } + + .my-filter { + display: none; + } + + + + + .blog-prev-text { + margin-left: 15px; + margin-top: 5px; + height: auto; + /* margin-bottom: 8px; */ + } + + /* Code blocks responsive */ + .code-block, + .terminal-block { + font-size: 13px; + padding: 0.8em 1em; + } + + /* Tables responsive - horizontally scrollable */ + table { + font-size: 14px; + display: block; + overflow-x: auto; + white-space: nowrap; + } + + th, td { + padding: 6px 4px; + } + + .desktop-only { + display: none; + } +} + +/* styles.css */ \ No newline at end of file diff --git a/Root-domain/brew.php b/Root-domain/brew.php new file mode 100644 index 0000000..7dd05a2 --- /dev/null +++ b/Root-domain/brew.php @@ -0,0 +1,14 @@ + diff --git a/Root-domain/clicky-images.js b/Root-domain/clicky-images.js new file mode 100644 index 0000000..fec6bc9 --- /dev/null +++ b/Root-domain/clicky-images.js @@ -0,0 +1,95 @@ + +/** + * + * @licstart The following is the entire license notice for the + * JavaScript code in this page. + * + * Copyright (C) 2025 David Polakovic + * + * + * The JavaScript code in this page is free software: you can + * redistribute it and/or modify it under the terms of the GNU + * General Public License (GNU GPL) as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) + * any later version. The code is distributed WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. + * + * As additional permission under GNU GPL version 3 section 7, you + * may distribute non-source (e.g., minimized or compacted) forms of + * that code without the copy of the GNU GPL normally required by + * section 4, provided you include this license notice and a URL + * through which recipients can access the Corresponding Source. + * + * @licend The above is the entire license notice + * for the JavaScript code in this page. + * + */ + +const dotImg = document.getElementById("toggleImageDot"); +const spaceImg = document.getElementById("toggleImageSpace"); +const dpoImg = document.getElementById("toggleImagedpo"); + +// Mouse behavior for dot +if (dotImg) { + dotImg.addEventListener("mousedown", () => { + dotImg.src = "../Pictures/dot2.png"; + }); + dotImg.addEventListener("mouseup", () => { + dotImg.src = "../Pictures/dot.png"; + }); + dotImg.addEventListener("mouseleave", () => { + dotImg.src = "../Pictures/dot.png"; + }); +} + +// Mouse behavior for space +if (spaceImg) { + spaceImg.addEventListener("mousedown", () => { + spaceImg.src = "../Pictures/space2.png"; + }); + spaceImg.addEventListener("mouseup", () => { + spaceImg.src = "../Pictures/space.png"; + }); + spaceImg.addEventListener("mouseleave", () => { + spaceImg.src = "../Pictures/space.png"; + }); +} + +// Mouse behavior for dpo +if (dpoImg) { + dpoImg.addEventListener("mousedown", () => { + dpoImg.src = "../Pictures/dpo2.png"; + }); + dpoImg.addEventListener("mouseup", () => { + dpoImg.src = "../Pictures/dpolakovic.png"; + }); + dpoImg.addEventListener("mouseleave", () => { + dpoImg.src = "../Pictures/dpolakovic.png"; + }); +} + +// Keyboard behavior +document.addEventListener("keydown", (event) => { + if (dotImg && event.key === ".") { + dotImg.src = "../Pictures/dot2.png"; + } + if (spaceImg && event.key === " ") { + spaceImg.src = "../Pictures/space2.png"; + } + if (dpoImg && event.key === "d") { + dpoImg.src = "../Pictures/dpo2.png"; + } +}); + +document.addEventListener("keyup", (event) => { + if (dotImg && event.key === ".") { + dotImg.src = "../Pictures/dot.png"; + } + if (spaceImg && event.key === " ") { + spaceImg.src = "../Pictures/space.png"; + } + if (dpoImg && event.key === "d") { + dpoImg.src = "../Pictures/dpolakovic.png"; + } +}); \ No newline at end of file diff --git a/Root-domain/error/error.php b/Root-domain/error/error.php new file mode 100644 index 0000000..4a4599f --- /dev/null +++ b/Root-domain/error/error.php @@ -0,0 +1,308 @@ + [ + 'title' => '100: Continue', + 'message' => 'Begin you may. Ready to receive more, the server is.' + ], + '101' => [ + 'title' => '101: Switching Protocols', + 'message' => 'Switch protocols, we shall. Agreed to your upgrade request, the server has.' + ], + '102' => [ + 'title' => '102: Processing', + 'message' => 'Processing your request, we are. Patience you must have. Time, this will take.' + ], + '103' => [ + 'title' => '103: Early Hints', + 'message' => 'Early hints, I give you. While the response prepares, preload resources you can.' + ], + + // ******* + // * 200 * + // ******* + '200' => [ + 'title' => '200: OK', + 'message' => 'What you sought, received you have.' + ], + '201' => [ + 'title' => '201: Created', + 'message' => 'Created, a new resource has been.' + ], + '202' => [ + 'title' => '202: Accepted', + 'message' => 'Accepted for processing, your request is. Complete yet, it is not. Patience, you must have.' + ], + '203' => [ + 'title' => '203: Non-Authoritative Information', + 'message' => 'Information you receive, but from the source directly, it comes not. Trust it carefully, you should.' + ], + '204' => [ + 'title' => '204: No Content', + 'message' => 'Successful, your request was. But content to show you, there is none.' + ], + '205' => [ + 'title' => '205: Reset Content', + 'message' => 'Reset your view, you must. Successful the request was, but refresh your document, you should.' + ], + '206' => [ + 'title' => '206: Partial Content', + 'message' => 'Only part of what you requested, delivered it is. The rest, await it does.' + ], + '207' => [ + 'title' => '207: Multi-Status', + 'message' => 'Multiple operations performed, we have. Different outcomes, each one has.' + ], + '208' => [ + 'title' => '208: Already Reported', + 'message' => 'Already reported in this response, these results were. Repeat them, we will not.' + ], + '226' => [ + 'title' => '226: IM Used', + 'message' => 'Instance manipulations applied, we have. Transformed, the resource is.' + ], + // ******* + // * 300 * + // ******* + '300' => [ + 'title' => '300: Multiple Choices', + 'message' => 'Many paths before you lie. Choose wisely, you must.' + ], + '301' => [ + 'title' => '301: Moved Permanently', + 'message' => 'Moved, this page has. Forever gone from here, it is. Follow the new path, you must.' + ], + '302' => [ + 'title' => '302: Found', + 'message' => 'Found elsewhere, what you seek is. Temporarily moved, it has. Follow, you should.' + ], + '303' => [ + 'title' => '303: See Other', + 'message' => 'Look elsewhere, you must. Another location, the answer awaits.' + ], + '304' => [ + 'title' => '304: Not Modified', + 'message' => 'Changed, nothing has. Your cached version, still good it is.' + ], + '305' => [ + 'title' => '305: Use Proxy', + 'message' => 'Through a proxy, your path must go. Direct access, allowed it is not.' + ], + + // ******* + // * 400 * + // ******* + '400' => [ + 'title' => '400: Bad Request', + 'message' => 'Understand your request, I cannot. Malformed it is, hmm.' + ], + '401' => [ + 'title' => '401: Unauthorized', + 'message' => 'Identify yourself, you must. Access without credentials, forbidden it is.' + ], + '402' => [ + 'title' => '402: Payment Required', + 'message' => 'Pay first, you must. Free, this resource is not' + ], + '403' => [ + 'title' => '403: Forbidden', + 'message' => 'Permission to enter, you have not. Forbidden this path is.' + ], + '404' => [ + 'title' => '404: Not Found', + 'message' => 'The page you seek, exist does not. Lost in the void, it is.' + ], + '405' => [ + 'title' => '405: Method Not Allowed', + 'message' => 'You must unlearn what you have learned. This method, the way it is not.' + ], + '406' => [ + 'title' => '406: Not Acceptable', + 'message' => 'Difficult to see. Always in motion, acceptable formats are. Yours, compatible is not.' + ], + '407' => [ + 'title' => '407: Proxy Authentication Required', + 'message' => 'Through the proxy, your journey goes. But authenticate you must, before pass you may.' + ], + '408' => [ + 'title' => '408: Request Timeout', + 'message' => 'Too long you have taken. Patience, the server has lost, hmm.' + ], + '409' => [ + 'title' => '409: Conflict', + 'message' => 'Fear leads to anger. Anger leads to conflict. With current state, your request conflicts.' + ], + '410' => [ + 'title' => '410: Gone', + 'message' => 'Once here it was. Gone forever now, it is.' + ], + '411' => [ + 'title' => '411: Length Required', + 'message' => 'Size matters not... but to the server, it does. Specify the length, you must.' + ], + '412' => [ + 'title' => '412: Precondition Failed', + 'message' => 'Met, your preconditions were not. Failed, the requirements have.' + ], + '413' => [ + 'title' => '413: Content Too Large', + 'message' => 'Too large, your request is. Smaller, you must make it.' + ], + '414' => [ + 'title' => '414: URI Too Long', + 'message' => 'Too long, your URL is. Shorten it, you must.' + ], + '415' => [ + 'title' => '415: Unsupported Media Type', + 'message' => 'Much to learn, you still have. This format, support the server does not.' + ], + '416' => [ + 'title' => '416: Range Not Satisfiable', + 'message' => 'Only what you take with you. Beyond the resource bounds, your range reaches.' + ], + '417' => [ + 'title' => '417: Expectation Failed', + 'message' => 'Meet your expectations, the server cannot. Expect less, perhaps you should.' + ], + '418' => [ + 'title' => '418: I\'m a teapot', + 'message' => 'A teapot, I am. Brew coffee, I cannot. Absurd this is, yes, hmmm!' + ], + '421' => [ + 'title' => '421: Misdirected Request', + 'message' => 'Wrong server, you have reached. Directed elsewhere, you should be.' + ], + '422' => [ + 'title' => '422: Unprocessable Content', + 'message' => 'Understand your request, I do. Process it correctly, I cannot.' + ], + '423' => [ + 'title' => '423: Locked', + 'message' => 'Locked, this resource is. Access it now, you cannot.' + ], + '424' => [ + 'title' => '424: Failed Dependency', + 'message' => 'Failed, a dependency has. Complete your request, I cannot.' + ], + '425' => [ + 'title' => '425: Too Early', + 'message' => 'Hasty, you are. Patience, the Force requires. Too early, your request comes.' + ], + '426' => [ + 'title' => '426: Upgrade Required', + 'message' => 'Upgrade your protocol, you must. Old version, no longer supported is.' + ], + '428' => [ + 'title' => '428: Precondition Required', + 'message' => 'Conditional, your request must be. Preconditions, you must include.' + ], + '429' => [ + 'title' => '429: Too Many Requests', + 'message' => 'Too eager you are. Slow down, you must. Many requests, too many!' + ], + '431' => [ + 'title' => '431: Request Header Fields Too Large', + 'message' => 'Train yourself to let go of everything you fear to lose. Too large, your headers are. Release some, you must.' + ], + '451' => [ + 'title' => '451: Unavailable For Legal Reasons', + 'message' => 'The dark side of the Force surrounds the law. By legal reasons, blocked this is. Available, it is not.' + ], + + // ******* + // * 500 * + // ******* + + '500' => [ + 'title' => '500: Internal Server Error', + 'message' => 'Wrong, something has gone. Know what it is, the server does not. Mysterious this error is.' + ], + '501' => [ + 'title' => '501: Not Implemented', + 'message' => 'Implement this feature, the server does not. Capable of it, it is not.' + ], + '502' => [ + 'title' => '502: Bad Gateway', + 'message' => 'The gateway, troubled it is. A response invalid, received it has.' + ], + '503' => [ + 'title' => '503: Service Unavailable', + 'message' => 'Available, the server is not. Overwhelmed or resting, it may be. Return later, you should.' + ], + '504' => [ + 'title' => '504: Gateway Timeout', + 'message' => 'For another server\'s response, waited too long I have. Answer me, it did not.' + ], + '505' => [ + 'title' => '505: HTTP Version Not Supported', + 'message' => 'Your HTTP version, too old it is. Support it, the server cannot.' + ], + '506' => [ + 'title' => '506: Variant Also Negotiates', + 'message' => 'Circular, the negotiation has become. A proper variant, choose we cannot.' + ], + '507' => [ + 'title' => '507: Insufficient Storage', + 'message' => 'No more space, there is. Full, the storage has become.' + ], + '508' => [ + 'title' => '508: Loop Detected', + 'message' => 'Around in circles, the server goes. Break free, we must.' + ], + '510' => [ + 'title' => '510: Not Extended', + 'message' => 'Further extensions required are. Provided, they were not.' + ], + '511' => [ + 'title' => '511: Network Authentication Required', + 'message' => 'Before proceed you may, authenticate with the network, you must. Required, it is.' + ], +]; + +if (isset($errors[$error_code])) { + $title = $errors[$error_code]['title']; + $message = $errors[$error_code]['message']; +} else { + $title = $error_code . ': Is not a valid code'; + $message = 'Stupid you are. Breed you should not.'; +} + +// Only set HTTP response code for 4xx and 5xx errors +if ($error_code >= 400) { + http_response_code((int)$error_code); +} + +// REMOVE THIS LINE - it was the duplicate causing the problem: +// http_response_code((int)$error_code); +?> + + + + + +<?php echo htmlspecialchars($error_code); ?> + + + +
+Master Yoda says +

+

+ +

+
+ + \ No newline at end of file diff --git a/Root-domain/example-page.php b/Root-domain/example-page.php new file mode 100755 index 0000000..89e5024 --- /dev/null +++ b/Root-domain/example-page.php @@ -0,0 +1,48 @@ + + + + + + + dpolakovic.space + + + + + + + + + + + + + + +
+

Ttile

+

+ punch line +
+
+ First paragraph. +

+

+ Every other paragraph. +

+
+ + +
+ +
+ + + diff --git a/Root-domain/php/config.php b/Root-domain/php/config.php new file mode 100755 index 0000000..0a818cc --- /dev/null +++ b/Root-domain/php/config.php @@ -0,0 +1,255 @@ +"; + + $connection = @fsockopen($domain, $port, $errno, $errstr, 5); + if ($connection) { + fclose($connection); + echo ""; + return true; + } else { + echo ""; + return false; + } +} + +// Show if git server is down or running +function serverStatus() { + $websiteUrl = "http://git.dpolakovic.space"; + if (isWebsiteOnline($websiteUrl)) { + echo 'Git server'; + } else { + echo 'Git server | offline'; + } +} + +// Pick random second sentence on index.php +function randomSentence() { + $file = "./php/rnd.txt"; + + // Read all lines from the file into an array + $lines = file($file, FILE_IGNORE_NEW_LINES); + + if ($lines !== false && !empty($lines)) { + + // Get a random line index + $randomIndex = array_rand($lines); + + // Display the random line + echo $lines[$randomIndex]; + + } + else { echo "this is an error message."; } +} + +// Print time of last update +function lastFileUpdate(...$files) { + $latestTimestamp = 0; + foreach ($files as $file) { + if (file_exists($file)) { + $fileTime = filemtime($file); + if ($fileTime > $latestTimestamp) { + $latestTimestamp = $fileTime; + } + } + } + // Format and print the date + if ($latestTimestamp > 0) { + $formattedDate = date('d/m/Y', $latestTimestamp); + echo "$formattedDate"; + } else { + echo "No file found"; + } +} + +// Counts lines... +function countFileLines($filename) { + if (!file_exists($filename)) { + echo "File not found: $filename
"; + return; + } + + $lineCount = count(file($filename)); + echo $lineCount; +} + +// Print footer on every page +function printFooter($usesJS, $usesCookies, ...$files) { + // Print JS status + echo "Copyright 2023-" . date("Y") . " David Polakovic. All publications licensed under "; + echo 'CC BY-SA 4.0 unless stated otherwise.
'; + echo $usesJS ? 'This page uses trivial JavaScript' : 'This page is JavaScript free'; + echo $usesCookies ? " and temporary PHPSESSID cookie. " : ". This page doesn't use cookies. "; + echo 'Source code available here under '; + echo 'GPLv3 license. '; + // Find the most recent modification date from the files + $latestTimestamp = 0; + foreach ($files as $file) { + if (file_exists($file)) { + $fileTime = filemtime($file); + if ($fileTime > $latestTimestamp) { + $latestTimestamp = $fileTime; + } + } + } + // Format and print the date + if ($latestTimestamp > 0) { + $formattedDate = date('d/m/Y', $latestTimestamp); + echo "Last update on: $formattedDate.
"; + } else { + echo "Last update on: No valid files found
"; + } + echo 'Server for this domain is '; + echo 'RFC 1464 and '; + echo 'RFC 2324 compliant.'; +} + +// Just like footer... +function printNavBar(){ + echo << + + + + +HTML; +} + +// Checks file size ( used in javascript blog) +function getFileSize($filename) { + if (file_exists($filename)) { + $sizeInBytes = filesize($filename); + $sizeInKB = $sizeInBytes / 1024; + return round($sizeInKB, 1); + } else { + return "Error: File '$filename' does not exist.\n"; + } +} + +// Cconverts RSS file into blog page +function printBlog2($rss_file) { + // Get the selected category from URL parameter (default to 'all') + $selected_category = isset($_GET['category']) ? $_GET['category'] : 'all'; + + // Load the XML file with namespace handling + $xml = simplexml_load_file($rss_file, 'SimpleXMLElement', LIBXML_NOCDATA); + + // Check if the XML is loaded successfully + if ($xml !== false) { + // Register the content namespace if it exists + $xml->registerXPathNamespace('content', 'http://purl.org/rss/1.0/modules/content/'); + + // Loop through each tag in the RSS feed + foreach ($xml->channel->item as $item) { + // Get the category + $category = (string) $item->category; + + // Skip this item if it doesn't match the selected category + if ($selected_category !== 'all' && $category !== $selected_category) { + continue; + } + + $title = (string) $item->title; + $link = (string) $item->link; + $description = (string) $item->description; + $pubDate = (string) $item->pubDate; + $image_url = (string) $item->image; + $pos = (string) $item->pos; + + // Output + echo "
" . + ""; + echo "
" . + "$title
"; + echo substr($pubDate, 5, 12) . "
"; + echo "$description

"; + } + } else { + // If the XML file fails to load + echo "Failed to load the blogs. The rss.xml file is either missing or corrupted."; + } +} + +// Checks current TorBrowser version +function getTorBrowserVersion() { + $cacheFile = sys_get_temp_dir() . '/tor_browser_version_cache.txt'; + $cacheTime = 3600; // 1 hour in seconds + + // Check if cache file exists and is still valid + if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $cacheTime)) { + // Read from cache + $cachedData = file_get_contents($cacheFile); + echo $cachedData; + return; + } + + // If cache exists but is expired, delete it + if (file_exists($cacheFile)) { + unlink($cacheFile); + } + + // Tor Project blog page with release announcements + $url = 'https://blog.torproject.org/category/applications/'; + + // Initialize cURL + $ch = curl_init(); + + // Set cURL options + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); + + // Execute request + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + // Check for errors + if ($response === false || $httpCode !== 200) { + // If fetch fails, just show "." + echo "."; + return; + } + + // Search for the pattern "New Release: Tor Browser" followed by version number + if (preg_match('/New Release: Tor Browser\s+([\d.]+[a-z]?\d*)/i', $response, $matches)) { + $version = $matches[1]; + $output = " (Current version is $version)."; + } else { + $output = "."; + } + + // Save to cache + file_put_contents($cacheFile, $output); + + // Output the result + echo $output; +} + +?> \ No newline at end of file diff --git a/Root-domain/php/leaderboard.php b/Root-domain/php/leaderboard.php new file mode 100755 index 0000000..9dc8215 --- /dev/null +++ b/Root-domain/php/leaderboard.php @@ -0,0 +1,74 @@ +Please respect one entry per player.
Thank you."; + header("location: ../6C6561646572626F617264.php"); + exit(); + } +} + +// Regular expressions for checking the key_string +$key_stringPattern1 = '/^[^x]*$/'; // No lowercase 'x' +$key_stringPattern2 = '/^[^,]*,[^,]*,[^,]*$/'; // Exactly two commas +$key_stringPattern3 = '/^[^2]*2[^2]*2[^2]*$/'; // Exactly two '2's +$key_stringPattern4 = '/^[^W]*W[^W]*$/'; // Exactly one 'W' +$key_stringPattern5 = '/^[^\.]*\.[^\.]*$/'; // Exactly one period (.) +$key_stringPattern6 = '/^[^,]*$/'; // No commas +$key_stringPattern7 = '/^[^.]*$/'; // No dots +$key_stringPattern62 = '/^.{62}$/'; + +// Check if key_string matches all patterns except for one +$matchesPattern1 = preg_match($key_stringPattern1, $key_string); +$matchesPattern2 = preg_match($key_stringPattern2, $key_string); +$matchesPattern3 = preg_match($key_stringPattern3, $key_string); +$matchesPattern4 = preg_match($key_stringPattern4, $key_string); +$matchesPattern5 = preg_match($key_stringPattern5, $key_string); +$matchesPattern6 = preg_match($key_stringPattern6, $key_string); +$matchesPattern7 = preg_match($key_stringPattern7, $key_string); +$matchesPattern62 = preg_match($key_stringPattern62, $key_string); + +// If it matches all patterns except for the "exactly two commas" or "exactly one period" pattern +if ( + ($matchesPattern62 && $matchesPattern1 && $matchesPattern3 && $matchesPattern4 && $matchesPattern5 && $matchesPattern6) || + ($matchesPattern62 && $matchesPattern1 && $matchesPattern2 && $matchesPattern3 && $matchesPattern4 && $matchesPattern7) +) { + $file = fopen("hof.txt", "a"); + fwrite($file, "

" . $solver . "
" . $message . " (" . $currentDate . ")

\n"); + fclose($file); + + // Append the key_string to usedkeys.txt + file_put_contents($usedKeysFile, $key_string . "\n", FILE_APPEND | LOCK_EX); + + $_SESSION['message'] = ''; + header("location: ../6C6561646572626F617264.php"); +} else { + $_SESSION['message'] = "Ah ah ah, you didn't say the magic word!"; + header("location: ../6C6561646572626F617264.php"); +} +?> + + diff --git a/Tor-site/Styles/FONT_LICENSE.txt b/Tor-site/Styles/FONT_LICENSE.txt new file mode 100644 index 0000000..6843f31 --- /dev/null +++ b/Tor-site/Styles/FONT_LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Tor-site/Styles/NotoSerif-Bold.ttf b/Tor-site/Styles/NotoSerif-Bold.ttf new file mode 100755 index 0000000..aa21d44 Binary files /dev/null and b/Tor-site/Styles/NotoSerif-Bold.ttf differ diff --git a/Tor-site/Styles/NotoSerif-Italic.ttf b/Tor-site/Styles/NotoSerif-Italic.ttf new file mode 100755 index 0000000..786ecc7 Binary files /dev/null and b/Tor-site/Styles/NotoSerif-Italic.ttf differ diff --git a/Tor-site/Styles/NotoSerif-Regular.ttf b/Tor-site/Styles/NotoSerif-Regular.ttf new file mode 100755 index 0000000..dc30297 Binary files /dev/null and b/Tor-site/Styles/NotoSerif-Regular.ttf differ diff --git a/Tor-site/Styles/styles.css b/Tor-site/Styles/styles.css new file mode 100755 index 0000000..126828c --- /dev/null +++ b/Tor-site/Styles/styles.css @@ -0,0 +1,173 @@ +/* Define the font faces */ +@font-face { + font-family: 'Noto Serif'; + src: url('/Styles/NotoSerif-Regular.ttf') format('truetype'); + font-weight: 450; + font-style: normal; +} + +@font-face { + font-family: 'Noto Serif'; + src: url('/Styles/NotoSerif-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Noto Serif'; + src: url('/Styles/NotoSerif-Italic.ttf') format('truetype'); + font-weight: normal; + font-style: italic; +} + +body { + background-color: #E8CBA9;; + color: #000000; + font-family: 'Noto Serif', serif; + font-size: 18px; +} + +table { + border-collapse: collapse; + border: 1px solid grey; + width: 100%; + font-size: 16px; +} + +th { + background-color: #c69879; + border: 1px solid grey; +} + +td { + padding: 2px; + border: 1px solid grey; +} + +tr:hover { + background-color: #efdac3; +} + +nav { + display: flex; +} + +.nav-bar > ul { +list-style-type: none; +max-width: 1000px; +width: 100%; +margin: 0 auto; +padding: 0; +background-color: #333333; +border-radius: 10px; +} + +.nav-bar li { +float: left; +position: relative; +} + +.nav-bar li a { +display: block; +color: white; +text-align: center; +padding: 14px 16px; +text-decoration: none; +} + +.title-with-icon { + display: flex; + align-items: center; + gap: 15px; /* space between text and image */ +} + +.banner { + width: 100%; + max-width: 1000px; + margin: 0 auto; + overflow: visible; +} + +.banner p { + white-space: nowrap; + margin: 1em 0; + padding: 0; + overflow: visible; +} + +.banner img { + max-width: none; + display: inline-block; +} + +.content { + max-width: 975px; + height: auto; + width: 100%; + margin: 0 auto; + text-align: justify; + line-height: 1.25; +} + +.less { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.content img { + max-width: 100%; +} + +.hide-link { + color: inherit; + text-decoration: inherit; +} + +.footer { + max-width: 980px; + width: auto; + margin: 0 auto; + padding: 10px; + text-align: center; + background-color: #b38265; + font-size: 13px; + color: white; + border-radius: 10px; + +} + +.footer a { + color: white; +} + +.text-area { + font-family: monospace; + white-space: pre; +} + +.fresh-link:visited { + color: rgb(0, 0, 238); +} + +.terminal-block { + background: #000; + border: 1px solid #000; + border-left: 3px solid #000; + color: white; + font-weight: bold; + page-break-inside: avoid; + font-family: monospace; + font-size: 15px; + line-height: 1.6; + margin-bottom: 1.6em; + max-width: 100%; + overflow-x: auto; + white-space: nowrap; + padding: 1em 1.5em; + display: block; + word-wrap: break-word; + white-space: pre-wrap; +} diff --git a/Tor-site/dead-drop.php b/Tor-site/dead-drop.php new file mode 100755 index 0000000..b47cf65 --- /dev/null +++ b/Tor-site/dead-drop.php @@ -0,0 +1,69 @@ + + + + + + Dead drop + + + + + + + + + + + + + + + + + +
+

+

Dead drop

+ A technique used for anonymous information exchange. +
+
+ Enter the coordinates of a dead drop (a place) to check if it has stashed message or not. + Contents of messages are encrypted with AES-256-CBC. The encryption key are the location coordinates itself. + Each location entry is then hashed with SHA-256 to hide it from server owner and + webmaster (me). +


+

+

+ +

+
+

+ + + Every message gets automaticaly deleted after 3 days. +

+
+

+ +
+ + +
+ +
+

+ + + diff --git a/Tor-site/example-page.php b/Tor-site/example-page.php new file mode 100755 index 0000000..1a0a152 --- /dev/null +++ b/Tor-site/example-page.php @@ -0,0 +1,45 @@ + + + + + + Onion site + + + + + + + +
+

+

Title

+ punch line +

+ First paragraph. +

+

+ Every other paragraph. +

+
+ +
+ +
+ +
+
+ + diff --git a/Tor-site/php/captcha.php b/Tor-site/php/captcha.php new file mode 100644 index 0000000..33b9e77 --- /dev/null +++ b/Tor-site/php/captcha.php @@ -0,0 +1,71 @@ +Wrong captcha. Try again.

"; + } + + echo "
"; + echo "
"; + echo ""; + echo ""; + generatePicture(); + echo "
"; + echo ""; + echo "
"; + echo "


"; + return null; +} + +function generatePicture() { + $width = 95; + $height = 30; + + $image = imagecreatetruecolor($width, $height); + + $isWhite = rand(0, 1) === 1; + $bgColor = $isWhite ? imagecolorallocate($image, 255, 255, 255) : imagecolorallocate($image, 0, 0, 0); + imagefilledrectangle($image, 0, 0, $width, $height, $bgColor); + + $charset = 'abcdefghijklmnpqrstuvwxyz123456789'; + $randomStr = ''; + for ($i = 0; $i < 4; $i++) { + $randomStr .= $charset[rand(0, strlen($charset) - 1)]; + } + + $_SESSION['captcha'] = $randomStr; + + $fontSize = 5; + $charWidth = imagefontwidth($fontSize); + $charHeight = imagefontheight($fontSize); + $textWidth = $charWidth * strlen($randomStr); + $x = ($width - $textWidth) / 2; + $y = ($height - $charHeight) / 2; + + $colors = [ + imagecolorallocate($image, 255, 0, 0), + imagecolorallocate($image, 0, 0, 255), + imagecolorallocate($image, 0, 255, 0), + imagecolorallocate($image, 255, 0, 255), + imagecolorallocate($image, 0, 255, 255) + ]; + + for ($i = 0; $i < strlen($randomStr); $i++) { + $char = $randomStr[$i]; + $charColor = $colors[array_rand($colors)]; + imagestring($image, $fontSize, $x + $i * $charWidth, $y, $char, $charColor); + } + + ob_start(); + imagepng($image); + $imageData = ob_get_clean(); + imagedestroy($image); + + $base64 = base64_encode($imageData); + echo 'Random Image'; +} diff --git a/Tor-site/php/config.php b/Tor-site/php/config.php new file mode 100755 index 0000000..fa3473e --- /dev/null +++ b/Tor-site/php/config.php @@ -0,0 +1,88 @@ + $newestTimestamp) { + $newestTimestamp = $timestamp; + } + } + } + + if ($newestTimestamp === null) { + return "NO DATA"; + } + + return date("d/m/Y", $newestTimestamp); +} + +function printFooter($usesJS, $usesCookies, ...$files) { + // Print JS status + echo "Copyright 2025-" . date("Y") . " David Polakovic. All publications licensed under "; + echo 'CC BY-SA 4.0[*] unless stated otherwise.
'; + echo $usesJS ? 'This page uses trivial JavaScript[*]' : 'This page is JavaScript free'; + echo $usesCookies ? " but uses temporary PHPSESSID cookie. " : ". This page doesn't use cookies. "; + echo 'Source code available here[*] under '; + echo 'GPLv3 license[*]. '; + // Find the most recent modification date from the files + $latestTimestamp = 0; + foreach ($files as $file) { + if (file_exists($file)) { + $fileTime = filemtime($file); + if ($fileTime > $latestTimestamp) { + $latestTimestamp = $fileTime; + } + } + } + // Format and print the date + if ($latestTimestamp > 0) { + $formattedDate = date('d/m/Y', $latestTimestamp); + echo "Last update on: $formattedDate.
"; + } else { + echo "Last update on: No valid files found
"; + } + echo 'Hyperlinking to the clear web is marked with [*].'; +} + +function getTorBrowserVersion() { + // Tor Project blog page with release announcements + $url = 'https://blog.torproject.org/category/applications/'; + + // Initialize cURL + $ch = curl_init(); + + // Set cURL options + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); + + // Execute request + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + curl_close($ch); + + // Check for errors + if ($response === false || $httpCode !== 200) { + echo "."; + return; + } + + // Search for the pattern "New Release: Tor Browser" followed by version number + // Pattern matches: "New Release: Tor Browser 15.0.4" or similar + if (preg_match('/New Release: Tor Browser\s+([\d.]+[a-z]?\d*)/i', $response, $matches)) { + $version = $matches[1]; + echo " (Current version is $version)."; + } else { + echo "."; + } +} + +?> \ No newline at end of file diff --git a/Tor-site/php/dd2.php b/Tor-site/php/dd2.php new file mode 100644 index 0000000..374264b --- /dev/null +++ b/Tor-site/php/dd2.php @@ -0,0 +1,308 @@ + +If you don\'t know the W3W coordinates for location you want to check, +just click on the question mark next to the search box. +

+

+'); + +require_once 'captcha.php'; + +// === main.php === + +function countActiveDrops() { + if (is_dir(DEAD_DROP_DIR)) { + $files = scandir(DEAD_DROP_DIR); + $files = array_diff($files, array('.', '..')); + $fileCount = count($files); + + if ($fileCount == 0) { + echo "There is no message stored on this domain right now."; + } elseif ($fileCount == 1) { + echo "There is exactly one message stored on this domain right now."; + } else { + echo "There are $fileCount messages stored on this domain right now."; + } + } else { + echo "Directory '" . DEAD_DROP_DIR . "' does not exist."; + } +} + +function cleanTheCity() { + $now = time(); + if (is_dir(DEAD_DROP_DIR)) { + $files = scandir(DEAD_DROP_DIR); + foreach ($files as $file) { + if (in_array($file, IGNORED_DROPS)) continue; + + $filePath = DEAD_DROP_DIR . DIRECTORY_SEPARATOR . $file; + if (is_file($filePath)) { + $fileAge = $now - filemtime($filePath); + if ($fileAge > DROP_EXPIRY_SECONDS) { + unlink($filePath); + } + } + } + } else { + echo "

Error: Folder " . DEAD_DROP_DIR . " not found.

"; + } +} + +function deadDropUI() { + $session_id_key = 'csrf_token_session_id'; + $text_bottom = TEXT_BOTTOM; + + if (!isset($_SESSION['csrf_token'], $_SESSION['csrf_token_time'], $_SESSION[$session_id_key]) || + session_id() !== $_SESSION[$session_id_key] || + time() - $_SESSION['csrf_token_time'] > CSRF_TOKEN_LIFETIME) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + $_SESSION['csrf_token_time'] = time(); + $_SESSION[$session_id_key] = session_id(); + } + + $token = $_SESSION['csrf_token']; + $result = deadDropLogic(); + $dropFiles = glob(DEAD_DROP_DIR . '/*'); + $dropCount = count($dropFiles); + + if ($result['mode'] === 'readonly' && $result['status'] === 'cold') { + if ($dropCount > DROP_LIMIT) { + echo "
There is too much heat on this domain right now to stash a new message. Come back later.
"; + return; + } + $slowDown = checkDropRate(); + if ($slowDown) { + if (captcha($token, $result['w3w']) !== 'YAY') return; + } + echo "

" . htmlentities($result['w3w']) ."

This drop is cold. You can stash your message here.


"; + echo << +
+ + +
+ +
+

+ +HTML; + return; + } + + if ($result['mode'] === 'initial' || + ($result['mode'] === 'readonly' && $result['status'] === 'invalid')) { + echo << + +
+ + + + +
+ +

+$text_bottom +

+ +HTML; + } + + if ($result['mode'] === 'readonly') { + if ($result['status'] === 'hot') { + echo "
" . htmlspecialchars($result['decrypted'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "
"; + echo << +
+ + + +


+ + +HTML; + } elseif ($result['status'] === 'invalid') { + echo "

{$result['message']}
"; + } + } elseif ($result['mode'] === 'stashed') { + echo "

Message stashed at " . htmlentities($result['w3w']) ."

Your dead drop is hot. Continue.


"; + } + + +} + +function deadDropLogic() { + $session_id_key = 'csrf_token_session_id'; + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'], $_SESSION['csrf_token_time'], $_SESSION[$session_id_key]) || + $_POST['csrf_token'] !== $_SESSION['csrf_token'] || + session_id() !== $_SESSION[$session_id_key] || + time() - $_SESSION['csrf_token_time'] > CSRF_TOKEN_LIFETIME) { + die('CSRF token invalid or expired. Reload the website, that usually helps...'); + } + + if (isset($_POST['burnW3W'])) { + $input = trim($_POST['burnW3W']); + $hashed = hash('sha256', $input); + if (!isValidHash($hashed)) die('Invalid hash'); + $file = DEAD_DROP_DIR . '/' . $hashed; + if (file_exists($file)) unlink($file); + header("Location: http://apebkcjcj7da3f7hfpkbp4csylu53fqrbfcw6tb7n4os3efyjbsa4cyd.onion/dead-drop.php"); + exit; + } + + if (isset($_POST['stashContent'], $_POST['originalW3W'])) { + $input = trim($_POST['originalW3W']); + $hashed_w3w = hash('sha256', $input); + if (!isValidHash($hashed_w3w)) die('Invalid hash'); + $plaintext = $_POST['stashContent']; + + if (strlen($plaintext) > 4096) { + return ['mode' => 'readonly', 'w3w' => $input, 'status' => 'invalid', 'message' => 'ERROR: Message too long.']; + } + + $filepath = DEAD_DROP_DIR . "/{$hashed_w3w}"; + if (file_exists($filepath)) { + return ['mode' => 'readonly', 'w3w' => $input, 'status' => 'hot']; + } + + $cipher = "AES-256-CBC"; + $key = hash('sha256', $input); + $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)); + $encrypted = openssl_encrypt($plaintext, $cipher, $key, 0, $iv); + $output = base64_encode($iv . $encrypted); + file_put_contents($filepath, $output); + return ['mode' => 'stashed', 'w3w' => $input]; + } + + if (isset($_POST['inputString'])) { + $input = trim($_POST['inputString']); + if (!validateString($input)) { + return ['mode' => 'readonly', 'w3w' => $input, 'status' => 'invalid', 'message' => 'ERROR: Invalid coordinates format.']; + } + if (!validateStringW3W($input)) { + return ['mode' => 'readonly', 'w3w' => $input, 'status' => 'invalid', 'message' => 'ERROR: Place you are looking for doesn\'t exist.']; + } + $hashed = hash('sha256', $input); + if (!isValidHash($hashed)) die('Invalid hash'); + $dropPath = DEAD_DROP_DIR . '/' . $hashed; + if (file_exists($dropPath)) { + $raw = file_get_contents($dropPath); + $data = base64_decode($raw); + $iv_len = openssl_cipher_iv_length("AES-256-CBC"); + $iv = substr($data, 0, $iv_len); + $ciphertext = substr($data, $iv_len); + $key = hash('sha256', $input); + $decrypted = openssl_decrypt($ciphertext, "AES-256-CBC", $key, 0, $iv); + return [ + 'mode' => 'readonly', + 'w3w' => $input, + 'status' => 'hot', + 'decrypted' => $decrypted + ]; + } else { + return [ + 'mode' => 'readonly', + 'w3w' => $input, + 'status' => 'cold' + ]; + } + } + } + + return ['mode' => 'initial']; +} + +function isValidHash($hash) { + return preg_match('/^[a-f0-9]{64}$/', $hash) === 1; +} + +function validateString($str) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') return false; + if (substr($str, 0, 3) !== "///") return false; + $parts = substr($str, 3); + if (substr_count($parts, ".") !== 2) return false; + if (!preg_match('/^[a-z]+\.[a-z]+\.[a-z]+$/', $parts)) return false; + return true; +} + +function validateStringW3W($str) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') return false; + $cleaned = substr($str, 3); + $url = 'https://api.what3words.com/v3/autosuggest?input=' . urlencode($cleaned) . '&key=' . urlencode(API_KEY); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = curl_exec($ch); + if (curl_errno($ch)) { + echo "Curl error: " . curl_error($ch); + return false; + } + curl_close($ch); + $result = json_decode($response, true); + if (isset($result['suggestions']) && count($result['suggestions']) > 0) { + foreach ($result['suggestions'] as $suggestion) { + if (strcasecmp($suggestion['words'], $cleaned) === 0) return true; + } + } + return false; +} + +function checkDropRate() { + $files = glob(DEAD_DROP_DIR . '/*'); + usort($files, function($a, $b) { + return filemtime($b) - filemtime($a); + }); + + $now = time(); + $recentFiles = array_filter($files, fn($f) => $now - filemtime($f) <= 60); + + if (count($recentFiles) > 50) return true; + + if (count($files) >= 2) { + $t1 = filemtime($files[0]); + $t2 = filemtime($files[1]); + if (abs($t1 - $t2) < 2) return true; + } + + return false; +}